30、重构实战案例:一个完整的电商系统重构过程,从分析到落地。
终于到了最后一章。说实话,写到这里我有点感慨。前面29章我们聊了各种重构手法、设计原则、坏味道识别,但很多朋友会问:「这些零散的知识,到底怎么用在一个真实项目里?」
嗯,这一章我们就来干一件实在的事——把一个虚构但足够真实的电商系统,从头到尾重构一遍。我会带着你走完从「发现问题」到「落地交付」的全过程。
背景:这个电商系统怎么了?
假设我们接手了一个运行两年的电商平台。业务方反馈:每次大促都出问题,加个新促销活动要两周,线上Bug越来越多。我看了下代码,嗯,典型的「快糙猛」遗留系统。
核心问题有这么几个:
- 订单模块:一个Service类3000行,什么逻辑都往里塞
- 促销引擎:if-else嵌套了8层,每次加活动都提心吊胆
- 库存扣减:直接操作数据库,并发一高就超卖
- 支付回调:逻辑散落在三个不同模块,经常漏处理
说白了,这就是个「牵一发而动全身」的烂摊子。但别怕,重构最怕的不是代码烂,而是没有章法地乱改。
第一步:分析现状,找到重构的「靶心」
我个人习惯,重构前先做三件事:
- 画出现有架构图——搞清楚模块间怎么耦合的
- 梳理核心业务流程——下单、支付、库存、促销,哪个环节最痛
- 统计变更频率——哪些代码改得最多,哪些地方Bug最多
结果发现:订单模块和促销模块的耦合最严重。每次加活动,订单逻辑就得跟着改。这就是典型的「霰弹式修改」坏味道。
核心结论:重构的优先级不是「哪里代码烂」,而是「哪里改动最频繁、风险最高」。先把痛点解决掉,再谈代码优雅。
第二步:设计目标架构
重构不是瞎改,你得先想清楚「改完之后长什么样」。我画了一张目标架构图,你看一眼就明白了:
你看,核心思路就三个:
- 服务拆分:订单、促销、支付各管各的,别搅在一起
- 异步解耦:非核心链路走消息队列,别让一个环节拖死全局
- 数据隔离:每个服务有自己的数据库,别搞共享库那一套
第三步:分阶段重构,别想一口吃成胖子
重构最忌讳的就是「大爆炸式重写」。我在项目中见过太多团队,一上来就说「我们重写吧」,结果写了半年还没上线,业务方已经疯了。
正确的做法是分阶段、小步快跑:
| 阶段 | 目标 | 时间 | 关键动作 |
|---|---|---|---|
| 第一阶段 | 促销模块解耦 | 2周 | 将促销逻辑从订单中抽离,用策略模式替代if-else |
| 第二阶段 | 库存优化 | 1周 | 引入Redis缓存库存,用Lua脚本保证原子扣减 |
| 第三阶段 | 支付回调统一 | 1周 | 将散落的支付逻辑收拢到支付服务,通过MQ通知订单 |
| 第四阶段 | 订单模块瘦身 | 2周 | 拆分大Service,引入领域模型 |
我的经验:每个阶段结束后,必须跑一遍完整的回归测试。别想着「改完再测」,那会死得很惨。我曾经有一次重构促销模块,自认为改得很完美,结果上线后满减活动全失效了——就是因为少了一个边界条件。
第四步:动手重构——促销模块的「策略模式」改造
我们拿促销模块举个例子。原来的代码长这样:
// 重构前:满屏if-else
public BigDecimal calculateDiscount(Order order) {
if (order.hasCoupon("满100减20")) {
// 逻辑A
} else if (order.hasCoupon("满200减50")) {
// 逻辑B
} else if (order.hasCoupon("第2件半价")) {
// 逻辑C
} else if (...) {
// 还有几十个...
}
return BigDecimal.ZERO;
}
你想想看,每次加一个新活动,就得在这里加一个else if。改的人多了,谁都不敢动这段代码——怕改漏了。
重构后,我们用策略模式:
// 重构后:策略模式
public interface DiscountStrategy {
boolean supports(Order order);
BigDecimal calculate(Order order);
}
// 每个活动一个策略类
public class FullReductionStrategy implements DiscountStrategy {
@Override
public boolean supports(Order order) {
return order.hasCoupon("满100减20");
}
@Override
public BigDecimal calculate(Order order) {
// 满减逻辑
}
}
// 策略工厂
public class DiscountStrategyFactory {
private List<DiscountStrategy> strategies;
public BigDecimal apply(Order order) {
return strategies.stream()
.filter(s -> s.supports(order))
.findFirst()
.map(s -> s.calculate(order))
.orElse(BigDecimal.ZERO);
}
}
这样做的好处很明显:
- 加新活动只需要新增一个类,不用改老代码
- 每个策略独立测试,互不影响
- 哪天要下线某个活动,直接删掉对应类就行
注意:策略模式不是银弹。如果你的活动规则之间有「叠加」关系(比如满减和打折同时生效),那策略模式就不够用了,得考虑责任链模式或者规则引擎。我在项目中就踩过这个坑,后来改成了Drools规则引擎才搞定。
第五步:库存扣减——从「直接操作DB」到「Redis+Lua」
原来的库存代码是这样的:
// 重构前:直接扣数据库,并发必超卖
public boolean deductStock(Long skuId, Integer quantity) {
Stock stock = stockMapper.selectBySkuId(skuId);
if (stock.getQuantity() >= quantity) {
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
return true;
}
return false;
}
嗯,这段代码在单机环境下没问题。但电商大促时,同一秒可能有几百个请求同时进来——超卖就发生了。
重构后,我们用Redis+Lua保证原子性:
// 重构后:Redis Lua脚本保证原子扣减
// Lua脚本内容
String luaScript =
"local stock = redis.call('get', KEYS[1]) " +
"if stock and tonumber(stock) >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
// Java调用
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList("stock:" + skuId),
quantity.toString());
为什么用Lua?因为Redis执行Lua脚本是原子性的,不会出现「读-改-写」之间的并发问题。说白了,就是把并发控制交给了Redis,而不是靠数据库锁。
第六步:支付回调统一——从「散落各处」到「事件驱动」
原来的支付回调逻辑散落在三个地方:订单模块里有一段、支付模块里有一段、甚至前端还直接调了个接口更新状态。每次排查问题都像破案。
重构后,我们统一走消息队列:
// 支付服务:收到回调后,只发消息
public void handlePaymentCallback(PaymentCallback callback) {
// 校验签名
if (!verifySign(callback)) {
throw new PaymentException("签名验证失败");
}
// 更新支付流水状态
paymentMapper.updateStatus(callback.getOrderId(), "PAID");
// 发送消息,通知订单服务
messageSender.send("order.payment.paid", callback.getOrderId());
}
// 订单服务:监听消息,更新订单状态
@RabbitListener(queues = "order.payment.paid")
public void onPaymentPaid(String orderId) {
orderService.markAsPaid(orderId);
// 触发后续流程:发货、发短信等
}
这样做的好处:支付服务和订单服务完全解耦。支付挂了不影响订单,订单挂了也不影响支付。而且消息队列有重试机制,偶尔网络抖动也不怕丢消息。
第七步:验证与上线——重构不是终点
重构完不等于完事了。我一般会做这几件事来验证:
- 对比测试:新老系统同时跑,对比每个接口的返回结果是否一致
- 压测:用JMeter模拟大促流量,看新系统能扛多少QPS
- 灰度发布:先让1%的流量走新系统,没问题再逐步放量
- 监控告警:把核心接口的耗时、错误率都配上告警,出问题第一时间知道
关键提醒:重构后的系统,性能不一定比原来好。因为加了抽象层、消息队列,反而可能变慢。但我们要的是「可维护性」和「可扩展性」。性能问题可以后续优化,但代码烂了,改都改不动。
总结:重构的「道」与「术」
回顾这个电商系统的重构过程,我想分享几点心得:
- 先分析,再动手——别上来就改代码,搞清楚哪里最痛
- 小步快跑,持续集成——每次改动都要能回滚,别搞大爆炸
- 用设计模式,但别滥用——策略模式、工厂模式够用就行,别整太复杂
- 测试是你的安全带——没有自动化测试的重构,就是在走钢丝
说实话,重构这件事,技术只占一半。另一半是勇气和耐心。勇气来自你敢改,耐心来自你愿意一步步来。希望这30章的内容,能让你在下次面对烂代码时,不再手足无措。
好了,课程到这里就结束了。如果你在实际项目中遇到什么重构难题,欢迎来找我聊聊。毕竟,代码这条路,我们都在不断重构自己。