从零到一:运费微服务实战笔记(含责任链模式优化与缓存设计)

张开发
2026/4/13 19:02:29 15 分钟阅读

分享文章

从零到一:运费微服务实战笔记(含责任链模式优化与缓存设计)
1. 运费微服务实战入门指南第一次接触运费计算功能时我也被各种复杂的业务规则搞得头晕眼花。不同地区的首重价格、续重阶梯、特殊区域附加费...这些规则如果全部写在同一个方法里代码很快就会变成难以维护的意大利面条。这就是为什么我们需要微服务架构——把运费计算这个业务能力单独抽离出来成为一个独立的服务。先来看一个最简单的运费计算场景假设我们只需要根据重量计算运费首重10元续重每公斤5元。用Java实现大概长这样public BigDecimal calculateFee(BigDecimal weight) { BigDecimal firstWeight new BigDecimal(1); BigDecimal firstFee new BigDecimal(10); BigDecimal extraFee new BigDecimal(5); if (weight.compareTo(firstWeight) 0) { return firstFee; } else { BigDecimal extraWeight weight.subtract(firstWeight); return firstFee.add(extraWeight.multiply(extraFee)); } }但现实情况要复杂得多。我接手过一个电商项目他们的运费规则包括按地区划分的经济区如华东区、华北区特殊节假日附加费会员等级折扣商品品类特殊规则当这些规则全部耦合在一起时每次修改运费政策都像是在拆炸弹——你永远不知道会引爆哪个隐藏的bug。这就是我们为什么要用微服务架构来解耦这个功能。2. 基础功能实现2.1 运费模板管理运费模板是运费计算的基础我们需要先实现模板的CRUD功能。这里我推荐使用MyBatis-Plus来快速开发基础数据操作。先看实体类设计Data TableName(carriage_template) public class CarriageTemplate { TableId(type IdType.AUTO) private Long id; private String name; private Long regionId; // 经济区ID private BigDecimal firstWeight; private BigDecimal firstFee; private BigDecimal extraFee; private Integer status; // 其他字段... }Controller层的实现要注意参数校验。我见过太多因为没做好校验导致的生产事故PostMapping(/templates) public Result addTemplate(Valid RequestBody CarriageTemplateDTO dto) { // 检查模板名称是否重复 if (carriageService.existsByName(dto.getName())) { throw new BusinessException(模板名称已存在); } return Result.success(carriageService.saveTemplate(dto)); }常见坑点重量单位不统一有的用kg有的用g价格精度问题一定要用BigDecimal不要用double经济区关联校验避免出现不存在的区域ID2.2 运费计算核心逻辑计算运费时最麻烦的是处理各种边界条件。比如重量刚好等于首重时重量为0或负数时区域不存在运费模板时这里分享一个经过实战检验的计算方法public BigDecimal calculate(Long regionId, BigDecimal weight) { // 1. 参数校验 if (weight null || weight.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(非法重量值); } // 2. 查询运费模板 CarriageTemplate template templateMapper.selectByRegion(regionId); if (template null) { throw new BusinessException(该区域不支持配送); } // 3. 计算逻辑 if (weight.compareTo(template.getFirstWeight()) 0) { return template.getFirstFee(); } else { BigDecimal extraWeight weight.subtract(template.getFirstWeight()); BigDecimal extraFee extraWeight.multiply(template.getExtraFee()); return template.getFirstFee().add(extraFee); } }3. 架构优化责任链模式当运费规则变得越来越复杂时if-else嵌套会让人崩溃。比如我们后来增加了偏远地区附加费大件商品特殊计费促销活动免运费这时候就该祭出责任链模式了。它的核心思想是把每个计费规则拆分成独立的处理器形成一个处理链条。3.1 责任链实现先定义处理器接口public interface CarriageHandler { boolean handle(CarriageContext context); int getOrder(); // 执行顺序 }然后实现具体处理器。比如偏远地区处理器Component RequiredArgsConstructor public class RemoteAreaHandler implements CarriageHandler { private final RemoteAreaService areaService; Override public boolean handle(CarriageContext context) { if (areaService.isRemoteArea(context.getRegionId())) { context.addFee(context.getBaseFee().multiply(new BigDecimal(0.2))); return true; } return false; } Override public int getOrder() { return 20; // 执行顺序 } }最后用Spring的自动装配构建责任链Component RequiredArgsConstructor public class CarriageChain { private final ListCarriageHandler handlers; public BigDecimal calculate(CarriageContext context) { handlers.stream() .sorted(Comparator.comparingInt(CarriageHandler::getOrder)) .forEach(handler - handler.handle(context)); return context.getTotalFee(); } }3.2 模式优势责任链模式带来的好处非常明显可扩展性新增规则只需添加新处理器不用修改现有代码可维护性每个规则逻辑独立便于测试和调试灵活性可以通过调整处理器顺序改变计算逻辑我在实际项目中用这种模式处理过包含15种不同规则的运费计算代码依然保持清晰。4. 性能优化缓存设计运费计算是个高频操作特别是电商促销时QPS可能达到几千。直接查数据库显然不现实这时候就需要引入缓存。4.1 Redis缓存策略我推荐使用Redis的Hash结构存储运费模板Key: 区域IDField: 模板属性firstWeight, firstFee等Value: 对应值public CarriageTemplate getTemplate(Long regionId) { String key carriage:template: regionId; // 先查缓存 MapObject, Object entries redisTemplate.opsForHash().entries(key); if (!entries.isEmpty()) { return mapToTemplate(entries); } // 缓存没有则查数据库 CarriageTemplate template templateMapper.selectByRegion(regionId); if (template ! null) { MapString, String map templateToMap(template); redisTemplate.opsForHash().putAll(key, map); redisTemplate.expire(key, 1, TimeUnit.HOURS); // 设置过期时间 } return template; }4.2 缓存一致性更新模板时要特别注意缓存同步问题。我采用先更新数据库再删除缓存的策略Transactional public void updateTemplate(CarriageTemplate template) { // 1. 更新数据库 templateMapper.updateById(template); // 2. 删除缓存 String key carriage:template: template.getRegionId(); redisTemplate.delete(key); }踩坑提醒不要用先删缓存再更新数据库在高并发下会导致脏数据考虑使用分布式锁防止缓存击穿对于特别热门的key可以设置不同的过期时间避免缓存雪崩5. 实战中的经验分享在多个项目中实现运费微服务后我总结了以下几点经验代码组织建议把运费规则配置化可以动态调整而不需要发版使用策略模式处理不同的计费方式按重量、按体积、按件数等为运费计算提供模拟模式方便测试性能优化技巧对热门区域模板进行预加载使用本地缓存Redis的多级缓存批量查询接口避免多次网络IO监控指标计算耗时分布缓存命中率各区域计算频次有一次大促前我们通过监控发现某个经济区的缓存命中率异常低排查后发现是该区域模板设置了过短的过期时间。这种问题只有实际运行中才会暴露出来。

更多文章