30、重构实战案例:一个完整的电商系统重构过程,从分析到落地。

终于到了最后一章。说实话,写到这里我有点感慨。前面29章我们聊了各种重构手法、设计原则、坏味道识别,但很多朋友会问:「这些零散的知识,到底怎么用在一个真实项目里?」

嗯,这一章我们就来干一件实在的事——把一个虚构但足够真实的电商系统,从头到尾重构一遍。我会带着你走完从「发现问题」到「落地交付」的全过程。

背景:这个电商系统怎么了?

假设我们接手了一个运行两年的电商平台。业务方反馈:每次大促都出问题,加个新促销活动要两周,线上Bug越来越多。我看了下代码,嗯,典型的「快糙猛」遗留系统。

核心问题有这么几个:

  • 订单模块:一个Service类3000行,什么逻辑都往里塞
  • 促销引擎:if-else嵌套了8层,每次加活动都提心吊胆
  • 库存扣减:直接操作数据库,并发一高就超卖
  • 支付回调:逻辑散落在三个不同模块,经常漏处理

说白了,这就是个「牵一发而动全身」的烂摊子。但别怕,重构最怕的不是代码烂,而是没有章法地乱改。

第一步:分析现状,找到重构的「靶心」

我个人习惯,重构前先做三件事:

  1. 画出现有架构图——搞清楚模块间怎么耦合的
  2. 梳理核心业务流程——下单、支付、库存、促销,哪个环节最痛
  3. 统计变更频率——哪些代码改得最多,哪些地方Bug最多

结果发现:订单模块和促销模块的耦合最严重。每次加活动,订单逻辑就得跟着改。这就是典型的「霰弹式修改」坏味道。

核心结论:重构的优先级不是「哪里代码烂」,而是「哪里改动最频繁、风险最高」。先把痛点解决掉,再谈代码优雅。

第二步:设计目标架构

重构不是瞎改,你得先想清楚「改完之后长什么样」。我画了一张目标架构图,你看一眼就明白了:

电商系统目标架构(重构后) API 网关(统一入口) 订单服务 促销服务 支付服务 订单数据库 促销缓存 + DB 支付流水表 消息队列(解耦异步逻辑)

你看,核心思路就三个:

  • 服务拆分:订单、促销、支付各管各的,别搅在一起
  • 异步解耦:非核心链路走消息队列,别让一个环节拖死全局
  • 数据隔离:每个服务有自己的数据库,别搞共享库那一套

第三步:分阶段重构,别想一口吃成胖子

重构最忌讳的就是「大爆炸式重写」。我在项目中见过太多团队,一上来就说「我们重写吧」,结果写了半年还没上线,业务方已经疯了。

正确的做法是分阶段、小步快跑:

阶段 目标 时间 关键动作
第一阶段 促销模块解耦 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);
    // 触发后续流程:发货、发短信等
}

这样做的好处:支付服务和订单服务完全解耦。支付挂了不影响订单,订单挂了也不影响支付。而且消息队列有重试机制,偶尔网络抖动也不怕丢消息。

第七步:验证与上线——重构不是终点

重构完不等于完事了。我一般会做这几件事来验证:

  1. 对比测试:新老系统同时跑,对比每个接口的返回结果是否一致
  2. 压测:用JMeter模拟大促流量,看新系统能扛多少QPS
  3. 灰度发布:先让1%的流量走新系统,没问题再逐步放量
  4. 监控告警:把核心接口的耗时、错误率都配上告警,出问题第一时间知道

关键提醒:重构后的系统,性能不一定比原来好。因为加了抽象层、消息队列,反而可能变慢。但我们要的是「可维护性」和「可扩展性」。性能问题可以后续优化,但代码烂了,改都改不动。

总结:重构的「道」与「术」

回顾这个电商系统的重构过程,我想分享几点心得:

  • 先分析,再动手——别上来就改代码,搞清楚哪里最痛
  • 小步快跑,持续集成——每次改动都要能回滚,别搞大爆炸
  • 用设计模式,但别滥用——策略模式、工厂模式够用就行,别整太复杂
  • 测试是你的安全带——没有自动化测试的重构,就是在走钢丝

说实话,重构这件事,技术只占一半。另一半是勇气和耐心。勇气来自你敢改,耐心来自你愿意一步步来。希望这30章的内容,能让你在下次面对烂代码时,不再手足无措。

好了,课程到这里就结束了。如果你在实际项目中遇到什么重构难题,欢迎来找我聊聊。毕竟,代码这条路,我们都在不断重构自己。

公众号:蓝海资料掘金营,微信 deep3321