news 2026/9/4 14:23:52

中小型商城系统重构:状态机与数据库平滑迁移实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中小型商城系统重构:状态机与数据库平滑迁移实践

zhshop 的这次重构,前后跨越了从“功能可用”到“结构清晰”的过程。项目本身是典型的中小型商城系统,早期为了快速跑通订单、商品、库存、支付链路,代码中积累了不少“先能用再说”的痕迹。重构完成之后,最直观的变化不是代码量减少,而是每接到一个新需求,团队能说清该去改哪个模块、动哪张表、走哪个状态流。对大多数中后台项目而言,这种判断力比代码本身更有价值。

这篇文章以 zhshop 的重构为线索,整理一套适合中小型商城的重构路径,覆盖重构边界、分支策略、模块拆分、状态机改造、数据库平滑迁移、回归验证和常见坑。文中示例技术栈以 Spring Boot、MyBatis、MySQL、Redis 为主,前端按 Vue 项目处理。如果你手里的同名项目或者类似项目技术栈不同,迁移思路仍然可以直接复用。

1. 重构完成不等于推倒重来:先定义边界

1.1 为什么中小商城最需要先定边界

商城系统有一个明显特点:业务链路长,但每个环节看起来都不复杂。用户注册、商品浏览、购物车、下单、支付回调、库存扣减、售后,单独拆出来都不难实现。真正的复杂度出现在链路交叉处:下单要扣库存,支付成功要改订单状态,订单取消要把库存还回来,售后完成可能还要发补偿消息。

如果不先定义重构边界,很容易把一次重构做成一次重写。重写意味着所有历史问题都需要在同一个版本里解决,风险会成倍增加。zhshop 这次重构首先确定的不是“要改哪些文件”,而是“哪些东西绝对不能变”:数据库已有数据不能丢,对外接口路径不能随意改,权限模型不能推倒,历史订单必须可查。把这些当成红线之后,代码改动方向才变得清晰。

从工程角度看,重构完成的标准不是“代码没有重复”,也不是“所有模块都换成最新框架”,而是系统从“加功能越来越难”变成“加功能有明确落点”。这个标准要写在重构计划的第一行。

1.2 重构前常见的三个危险信号

技术债不会突然消失,它通常会通过三类信号暴露出来。

第一个信号是一个核心方法过长且承担多个职责。比如订单 Service 里的createOrder方法可能同时负责参数校验、价格计算、库存预占、优惠券核销、创建订单、发送日志。方法行数超过 200 行后,每次改动都要从头到尾读一遍,因为谁也不敢确认哪个局部变量影响了后续判断。

第二个信号是相似代码散落在不同模块。比如支付回调处理中,微信回调写一遍签名校验,支付宝回调再复制一遍;库存扣减在订单模块和售后模块各写一遍 SQL。复制代码的短期效率很高,但它会带来两个问题:一是修改业务规则时要找全所有副本,二是不同副本可能在一段时间后产生细微差异。

第三个信号是模块之间反向依赖。表现通常是 Controller 直接调用其他模块的 Mapper,或者订单 Service 为了判断商品库存,直接拿到商品模块的内部数据对象。这种耦合在单体项目里特别容易发生,因为所有类都在同一个工程里,import 不需要额外审批。

如果项目已经出现这三种信号,重构的收益会很大。但要注意,收益是以“边界清晰”衡量的,不是以“代码行数减少”衡量的。

1.3 重构期间不能动的兼容红线

重构开始前,建议用一张表格守住兼容红线。哪些可以内部调整,哪些必须保持兼容,要提前对齐。zhshop 的反面教材是早期曾经在重构商品分类时顺手改了商品查询接口的返回字段,导致管理后台和客户端同时报错,最后用一整个晚上回滚排查。问题不是出在代码质量,而是出在“没有区分内部改动和外部影响”。

兼容红线至少包含下面几项:

  1. 数据库存量数据和字段语义。除非有明确的迁移脚本和回滚方案,否则不要删除旧字段。
  2. 对外接口的 URL、请求参数、返回结构。已有的 App、小程序、管理后台都是接口消费方,不能按内部设计自由改动。
  3. 权限模型的角色编码和权限粒度。如果旧系统使用adminoperator等编码,最少在第一个版本保留旧编码兼容。
  4. 消息和异步任务的格式。订单创建后的 MQ 消息、延迟取消任务、售后提醒,如果换了消息体,消费者可能反序列化失败。
  5. 日志和监控字段。线上排障时,很多团队已经习惯按orderNouserId搜索日志,重构时不要随意换字段名。

确定红线之后,所有重构任务都要做一道判断题:这个改动是新增还是修改,是内部改造还是外部影响。外部影响优先向后兼容,内部改造才允许放开手脚。

1.4 重构范围和验收标准

这里给出一个参考范围表,适用于 zhshop 一类的商城项目。具体粒度要根据团队能投入的人力和测试环境决定。

模块重构范围验收标准不影响兼容的内容
用户模块拆分 Service,统一异常和返回结构登录、注册、地址管理回归通过用户表结构保持兼容
商品模块明细查询缓存化,分类逻辑收敛商品列表、详情响应符合性能指标商品查询 API 保持兼容
订单模块用状态机替换散落的 if 判断订单全链路状态流转可追踪历史订单可查询
库存模块扣减逻辑统一,增加幂等控制并发扣减不超卖,失败可回补库存接口和库存语义不变
支付模块按渠道策略分发回调微信、支付宝回调处理稳定支付回调验签方式保持不变
管理系统前端与后端新 DTO 对齐,拆分页面组件核心页面冒烟通过操作路径尽量不改变

范围表写完并不代表结束,还需要补充“本轮不做”清单。例如:不升级 Spring Boot 大版本、不切换数据库、不重建前端工程、不做微服务拆分。把“不做”写清楚,能降低重构过程中被临时需求带偏的概率。

2. 准备阶段:分支、依赖和本地启动策略

2.1 分支模型:每一次改动都要可回退

重构中最怕的情况是:所有人都挤在同一个分支里,重构代码和业务修复混在一起,上线后出了问题不知道回滚哪一种变化。

zhshop 在重构期间采用了两条长期分支:main分支保持可发布状态,refactor/order-state-machine这类功能分支从main拉出。每个功能分支只做一个内聚的重构任务,例如“订单状态机替换”是一个分支,“库存扣减幂等”是另一个分支。代码评审合并到main之前,必须在当前分支上运行完整回归。

git checkout main git pull origin main git checkout -b refactor/order-state-machine # 开发完成后 git add . git commit -m "refactor: 订单状态由散落 if 改为状态机" git push origin refactor/order-state-machine

提交信息推荐使用type(scope): description格式。类似refactor(order)fix(stock)feat(pay),时间久了以后,通过git log --oneline就能快速还原每次改动意图。这一步不起眼,但重构周期长,没有清晰的提交历史,后人很难判断某个设计是为哪个场景引入的。

2.2 依赖版本统一,避免被无关问题干扰

重构过程中,最不值得花时间的就是“为什么换了依赖后本地启动失败”。这不是说依赖升级不重要,而是它不应该和业务重构混在同一批次里。项目重构的核心价值在业务结构和数据边界,不在框架版本数字。

在重构开始前,建议把所有依赖版本固化一次。以 Spring Boot 项目为例,最基础的是把版本属性集中管理。下面的pom.xml片段是占位示例,落地时务必使用当前项目已验证过的版本。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>11</java.version> <mybatis.spring.boot.version>2.3.2</mybatis.spring.boot.version> </properties>

如果项目已经升级到 Spring Boot 3.x,要特别注意javax.*jakarta.*包名的替换。这个差异会直接导致 Controller、Service、Mapper 全部编译失败。与其在重构中途处理这类大规模变更,不如先选定技术基线,再把业务重构放到基线之上。

2.3 本地配置与启动顺序

后端项目在本地运行时依赖 MySQL 和 Redis。为避免每个开发者的配置不一致,application.yml中的环境相关参数尽量放在application-local.yml或环境变量里。示例配置如下:

spring: datasource: url: jdbc:mysql://localhost:3306/zhshop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: zhshop_dev password: changeit redis: host: localhost port: 6379 timeout: 3000ms mybatis: mapper-locations: classpath:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true

配置中有两个细节容易忽略。一是map-underscore-to-camel-case,数据库字段order_state默认情况下映射不到 Java 属性orderState,需要开启驼峰映射或用@Results手动映射。二是 Redis 连接超时不要设置太长,否则依赖 Redis 的接口在 Redis 不可用时会把线程全部阻塞住。生产环境还应该配置连接池大小和空闲超时。

启动顺序建议是:先启动 MySQL 和 Redis,再用--spring.profiles.active=local启动后端,最后再启动前端。前端开发服务器和后端联调时要注意反向代理配置,避免跨域问题掩盖真实接口错误。

3. 核心重构:订单状态机、库存幂等和支付回调

3.1 按业务模块拆包,而不是按技术层拆包

早期的 zhshop 结构性问题是“先按技术分层,再额外堆模块”。典型表现是controllerservicemapper下面放着所有业务的类。这种做法在演示项目里没有问题,但在真实商城项目里会导致OrderControllerStockController可以毫无阻碍地互相访问私有方法。

重构后的包结构建议按业务模块划分,每个模块内再保留自己的控制层、应用层和数据访问层。

com.zhshop ├── common │ ├── exception │ └── model ├── user │ ├── controller │ ├── service │ ├── dal │ └── model ├── product │ ├── controller │ ├── service │ ├── dal │ └── model ├── order │ ├── controller │ ├── service │ ├── state │ ├── dal │ └── model ├── inventory │ ├── service │ ├── dal │ └── model └── pay ├── controller ├── handler ├── service └── model

包结构调整是重构中劳动量最大、技术含量最低的一步,但也是最容易在后续重构中受益的一步。因为 Java 默认访问控制只区分包内和包外,按业务模块拆包后,可以通过包访问权限来限制内部方法,而不是依赖团队纪律。

3.2 用状态机替换散落的订单状态判断

订单状态是电商系统里最容易乱掉的部分。常见错误写法是:

if (order.getStatus() == 1 && "pay_result".equals(event)) { if (order.getUserId().equals(...)) { order.setStatus(2); } } else if (order.getStatus() == 2 && order.getLogisticsNo() != null) { order.setStatus(3); }

一个问题会被拆成很多层 if,每次加状态都要担心漏掉某个组合。重构时先把状态和事件建模成枚举,再用一个状态机负责非法流转判断。

事件枚举:

public enum OrderEvent { PAY_SUCCESS, CANCEL, SHIP, CONFIRM }

状态机核心类:

@Component public class OrderStateMachine { private static final Map<OrderStatus, Map<OrderEvent, OrderStatus>> TRANSITIONS = new ConcurrentHashMap<>(); static { allow(OrderStatus.CREATED, OrderEvent.PAY_SUCCESS, OrderStatus.PAID); allow(OrderStatus.CREATED, OrderEvent.CANCEL, OrderStatus.CANCELLED); allow(OrderStatus.PAID, OrderEvent.SHIP, OrderStatus.SHIPPED); allow(OrderStatus.SHIPPED, OrderEvent.CONFIRM, OrderStatus.COMPLETED); } private static void allow(OrderStatus from, OrderEvent event, OrderStatus to) { TRANSITIONS .computeIfAbsent(from, k -> new ConcurrentHashMap<>()) .put(event, to); } public OrderStatus next(OrderStatus current, OrderEvent event) { Map<OrderEvent, OrderStatus> map = TRANSITIONS.get(current); if (map == null || !map.containsKey(event)) { throw new InvalidOrderStateException( String.format("订单状态流转非法: %s + %s", current, event)); } return map.get(event); } }

在支付成功业务中调用:

@Transactional(rollbackFor = Exception.class) public void paySuccess(String orderNo, Long payerId) { Order order = orderMapper.selectByOrderNoForUpdate(orderNo); OrderStatus target = orderStateMachine.next(order.getStatus(), OrderEvent.PAY_SUCCESS); order.setStatus(target); orderMapper.updateStatus(order.getOrderNo(), target, order.getStatus()); }

这里有两点值得解释。第一,状态机的最大价值不是“代码看起来高级”,而是把所有合法流转集中在一个位置,非法组合直接用异常暴露出来。第二,更新订单状态时不能只按orderNo更新,还要把旧状态作为更新条件,否则两个并发请求可能把状态覆盖错。

3.3 库存扣减的幂等与防超卖

库存模块重构遇到的最核心问题是并发扣减。最初版本可能先查询库存,在 Java 层判断库存是否充足,再更新。这个逻辑在低并发下没问题,高并发下会超卖。

重构后的 SQL 应该把判断和更新合并成一条原子语句:

UPDATE zhshop_stock SET stock = stock - #{count} WHERE product_id = #{productId} AND stock >= #{count}

影响行数为 1 表示扣减成功,为 0 表示库存不足或商品不存在。数据库行锁能解决超卖,但不能解决同一个下单请求被重复提交的问题。为了幂等,需要在应用层使用 Redis 锁或对请求幂等键做去重。

Redis 锁示例:

String lockKey = "lock:stock:" + productId; String requestId = UUID.randomUUID().toString(); Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 5, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(locked)) { throw new BizException("系统繁忙,请稍后重试"); } try { int rows = stockMapper.deduct(productId, count); if (rows == 0) { throw new BizException("库存不足"); } } finally { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), List.of(lockKey), requestId); }

这里不要使用先getdel的解锁方式,因为如果锁已经过期并被其他线程持有,当前线程会把别人的锁删掉。使用 Lua 脚本比较requestId再删除,能够避免误删。需要说明的是,分布式锁只是兜底手段,稳定场景下更推荐请求幂等表,通过数据库唯一键拦截重复下单。

3.4 支付回调按渠道策略分发

支付回调是另一个容易写满 if else 的地方。微信、支付宝、银联等渠道的区别在于验签方式、回调参数、处理结果,但业务结果最终都是“修改订单为已支付”或“记录支付失败原因”。

重构时提取一个渠道处理器接口:

public interface PayChannelHandler { PayChannel channel(); PayResult handle(PayNotifyRequest request); }

微信处理器负责验签并转换成内部回调模型:

@Component public class WechatPayNotifyHandler implements PayChannelHandler { @Override public PayChannel channel() { return PayChannel.WECHAT; } @Override public PayResult handle(PayNotifyRequest request) { // 渠道特定的验签、解密、异常处理 return new PayResult(true, request.getOrderNo(), "微信支付成功"); } }

分发器注入所有实现类,并构建渠道到处理器的映射:

@Service public class PayNotifyDispatcher { private final Map<PayChannel, PayChannelHandler> handlerMap; public PayNotifyDispatcher(List<PayChannelHandler> handlers) { this.handlerMap = handlers.stream() .collect(Collectors.toUnmodifiableMap(PayChannelHandler::channel, Function.identity())); } public PayResult dispatch(PayChannel channel, PayNotifyRequest request) { PayChannelHandler handler = handlerMap.get(channel); if (handler == null) { throw new UnsupportedPayChannelException("不支持的支付渠道: " + channel); } return handler.handle(request); } }

新增渠道时只需要增加一个实现类,不需要修改分发器。策略模式的好处是隔离新增渠道的代码影响面,保证“加一种支付方式不碰核心订单逻辑”。注意,如果项目使用比较老的 Spring 版本,List<PayChannelHandler>的注入方式同样可用,不需要让每个实现类自己注册到 Map。

4. 数据库平滑迁移:先兼容,再切换,最后清理

4.1 字段变更坚持“先加后删”

代码重构通常会伴随字段语义升级。比如订单状态从旧编码0,1,2切换成新状态机的CREATED、PAID、SHIPPED。如果直接修改原status字段,会在重构中途出现旧代码写1,新代码读CREATED的兼容性问题。

推荐的顺序是先在表里新增字段:

ALTER TABLE zhshop_orders ADD COLUMN order_state VARCHAR(32) NULL COMMENT '新订单状态,重构后使用' AFTER status;

新代码写order_state,老代码继续写status。两套字段在过渡期同时存在,通过代码开关控制读取逻辑。等到新状态全面验证通过后,再把旧status字段标记废弃,最后在下一个版本中删除。这种策略看起来多一条 SQL,实际上是在给重构留保险丝。

4.2 历史存量数据回填

字段新增后,要尽快把存量数据回填。回填之前,必须先建立旧值到新值的映射表。旧系统中的状态数字不代表新状态机的枚举,需要由业务负责人确认。

一个最小回填脚本可以是:

UPDATE zhshop_orders SET order_state = CASE status WHEN 0 THEN 'CREATED' WHEN 1 THEN 'PAID' WHEN 2 THEN 'SHIPPED' WHEN 3 THEN 'COMPLETED' ELSE order_state END WHERE order_state IS NULL;

执行前先查询映射是否覆盖所有旧值:

SELECT status, COUNT(*) FROM zhshop_orders GROUP BY status;

如果查询结果中出现没有映射的状态,比如99-1,不能盲目回填,必须先确认这些历史异常状态的含义。商城历史数据里经常存在“已取消但库存未归还”等脏状态,只有先识别出来,才能确定是改成CANCELLED还是单独标记。

4.3 数据校验与回滚脚本

数据操作必须有校验和回滚两层保障。校验最简单的方法是分别统计新旧字段的分布,确保转换前后订单数量一致。

SELECT COUNT(*) AS total_orders, COUNT(CASE WHEN order_state IS NULL THEN 1 END) AS missing_new_state FROM zhshop_orders;

回滚脚本的作用是让系统能够退回旧版本。如果不提前写回滚脚本,线上发现问题时,数据库可能已经被新代码改出大量order_state值。回滚时不是简单执行DELETE,而是把新增字段移除或保留为空。

ALTER TABLE zhshop_orders DROP COLUMN order_state;

这里要注意,如果新代码已经开始依赖order_state字段,直接删字段会造成应用启动失败。真正可回滚的路径应该是:代码保留读取新字段开关,开关关闭后启用老字段逻辑,确认无问题后,再在下一个版本删除字段。

5. 测试与验证:重构完成的判断依据

5.1 业务回归清单

重构完成后,团队最容易犯的错误是只验证“核心流程能跑通”。但商城系统的问题往往出现在异常分支和交叉流程。比如支付回调重复通知、用户取消订单和库存回补同时发生、售后完成后订单状态跳变。

回归清单至少要覆盖以下场景。

验证模块验证场景预期结果
用户登录、注册、地址增删改返回结构稳定,权限字段正确
商品商品列表、详情、上下架缓存命中后数据一致
订单正常下单、支付、发货、确认收货状态按状态机顺序流转
订单下单后取消、支付后取消能按事件流转或拒绝非法操作
订单重复支付回调幂等处理,订单状态只变更一次
库存秒杀式并发扣减同一商品不超卖,请求失败提示清晰
库存订单取消后回补库存回补数量与释放值一致
支付微信、支付宝回调和验签失败验签失败不修改订单,记录日志
售后已发货后退货流程状态流转和退款处理符合规则
历史数据旧状态订单查询详情可以展示新状态,不报空指针

回归用例不能只在本地跑。应该在测试环境完整跑一遍,并把结果记录提交到测试管理或者直接写入文档,方便回滚后重新验证。

5.2 接口冒烟测试

后端重构后,接口消费端是否感知变化,需要通过冒烟测试确认。以“下单接口”为例,可以使用 curl 做路径冒烟。下面的地址和参数是示例,实际项目要以网关或 OpenAPI 文档中的路径为准。

curl -X POST "http://localhost:8080/api/order/create" \ -H "Content-Type: application/json" \ -d '{ "productId": 1001, "count": 2, "addressId": 88 }'

冒烟测试不只关注响应码是否为 200,还要检查返回体中的业务码、订单号、金额和时间字段。推荐在开发阶段加入接口兼容性断言,比较重构前后两次调用的 JSON 结构。如果返回字段名从order_id变成orderNo,前端不感知,但已经破坏兼容。

如果项目有 Postman Collection 或 Apifox,可以在重构完成后批量执行接口用例。没有这类工具时,至少要保留一份 curl 或 Requests 脚本到scripts/smoke目录,避免每次回归都靠手工点页面。

5.3 并发压测和故障演练

对商城项目来说,重构后至少需要两个指标验证:接口响应时间、并发下系统是否出现数据错乱。其中数据错乱比响应时间更重要。

压测不要只压“单接口反复调用”,要构造真实业务链路。例如 50 个线程同时为 N 个用户创建订单,每个订单扣减同一批商品库存,结束后检查订单创建成功率、库存总量是否守恒、支付回调是否产生重复处理。

压测命令可以用 JMeter、wrk、k6 或 Gatling。如果只是快速验证库存不超卖,也可以写一个 JUnit 并发测试,使用CountDownLatch同时发起扣减。但要注意,单元级并发测试无法覆盖真实网络、连接池和 Redis 延迟,只能作为回归提示,不能替代集成压测。

故障演练则可以测试两类场景:一是 Redis 宕机后,接口是否快速失败而不是长时间阻塞;二是支付回调重复通知时,系统是否因为状态机非法流转直接抛异常。故障演练要在测试环境进行,记录系统日志、报警和失败提示,复盘后再调整超时和重试策略。

6. 重构过程中常见的坑与排查路径

6.1 状态机事件乱序导致合法操作被拒绝

状态机生效后,容易出现“合法用户操作被拒绝”的问题。典型场景是用户付款后,支付回调已经将订单从CREATED改为PAID,但用户页面仍保留在待支付页,又点了一次“取消订单”。此时系统收到CANCEL事件,而当前状态是PAID。按订单规则,支付成功后通常不能直接取消,而是需要走退款流程。

排查这个现象时,不能只查状态机配置。先看事件来源:用户点击、支付回调、定时任务、管理后台操作都可能触发同一个CANCEL。如果前置页面状态没有同步,用户界面就会展示过期状态。

解决方案是在 Controller 层先判断当前订单状态是否允许该操作,或者把状态机返回的非法流转异常统一捕获,并提示“当前订单状态不支持该操作”。不要把内部异常直接抛给前端,否则用户看到一条InvalidOrderStateException也没有处理路径。

6.2 事务方法自调用导致回滚失效

重构订单和库存时,开发容易把事务加到 Service 方法上。如果类内部两个方法互相调用,例如createOrder调用deductStock,而deductStock在同一个类中且带有@Transactional,自调用不会走 Spring 代理,注解不会生效。结果就是库存扣减失败后,下单流程可能仍然提交了部分数据。

解决方法有三种:

  1. deductStock放到独立的StockService类中,由 Spring 代理调用。
  2. createOrder方法上增加完整事务,将库存扣减和订单创建合入同一事务。
  3. 如果必须使用自调用,可以注入ApplicationContext获取代理对象,但推荐重构掉这种写法。

代码评审时,事务方法自调用是优先级最高的检查项,因为它在编译期不会暴露问题,只在业务异常时表现为数据不一致。

6.3 旧字段和新字段不一致,历史数据查询混乱

迁移期间,statusorder_state两个字段并存,最怕出现“新代码写order_state,部分旧定时任务还在写status”。一段时间后,同一条订单在新旧字段上是两个状态,列表查询无论用哪个都会漏数据。

排查这类问题时,第一步是做数据一致性检查:

SELECT order_no, status, order_state FROM zhshop_orders WHERE status != order_state LIMIT 20;

查询结果出现异常后,不应急于写修复脚本,而是要先找到写入旧状态的任务来源。通常是延迟取消任务、对账任务或者旧版管理后台端口仍在运行。只有把所有写入口收敛到新代码,数据不一致问题才会消失。

6.4 缓存和数据库数据不一致

商品模块重构过程中,如果为了提高查询性能加入了 Redis 缓存,很容易出现缓存与数据库不一致。常见错误是更新数据库后忘记删除缓存,或者先删除缓存后数据库更新失败。

更合理的做法是采用 Cache Aside 模式:更新数据库成功后删除缓存,下次读取时回源数据库并重建缓存。这里的顺序不能反过来,否则并发读请求可能在数据库更新前把旧数据加载回缓存。

此外,缓存键必须包含足够的业务维度。商品详情可以拆成product:detail:{id},商品库存不要和详情共用同一个缓存键,因为库存变化频率远高于基本信息。缓存过期时间也要设随机值,避免同一时间大量缓存同时失效造成数据库压力。

7. 重构完成后的工程化保障

7.1 代码评审清单

重构完成不只是“代码合并进 main”那一刻。在合并前,按照下面清单做一轮审查,能过滤掉大部分回归风险。

  • 是否还存在跨模块直接访问 Mapper 的代码。
  • 核心状态更新 SQL 是否带旧状态或版本号条件。
  • 新增或修改接口时,返回结构是否向后兼容。
  • @Transactional方法是否还存在自调用。
  • 缓存更新顺序是否为“先更新数据库,再删除缓存”。
  • Redis 锁删除是否使用 Lua 脚本并携带请求唯一标识。
  • 数据库迁移脚本是否有回滚脚本。
  • 状态机是否覆盖所有历史状态和异常事件。
  • 日志是否包含orderNouserId、渠道等关键信息。
  • 本地启动配置是否包含环境差异,是否误提交密码和密钥。

评审清单不用追求数量,重点是让每位开发者知道重构后的代码该按什么标准检查。

7.2 发布与回滚

重构后的发布要采用“小步发布”策略,而不是把一周改动一次性上线。即便是在单体项目里,也可以通过配置开关实现灰度。例如订单模块可以拆成“状态机逻辑”和“新状态字段”两个开关,先打开字段迁移脚本,再切换业务读路径,最后再启用状态机。

回滚方案必须在发布前写清楚。比较安全的顺序是:

  1. 回滚代码到上一个可发布版本。
  2. 关闭新状态字段的读取开关。
  3. 保留新增字段,不删除数据库列。
  4. 确认旧逻辑恢复后,再分析失败原因。

不要在线上直接执行DROP COLUMN回滚,数据库回滚永远慢于代码回滚。数据字段宁可保留一段时间,也不要因为清理字段造成不可逆损失。

7.3 后续演进方向

zhshop 这类商城系统完成第一轮重构后,后续可以继续在几个方向做结构化改造。

一是把订单模块中的支付、库存、物流等交互收敛到领域事件。比如“订单已支付”事件由订单模块发出,库存、积分、消息通知模块订阅。这样可以进一步降低模块间的同步等待。

二是引入开放的 API 规范文档,将接口契约和代码同步管理。商城系统的前端、App、小程序都在变,而接口是稳定契约,把契约版本化能减少沟通成本。

三是为高频查询建立可观测指标。商品详情、购物车、订单列表都是高流量接口,至少统计 P99 响应时间、慢查询数、缓存命中率和异常率。重构是否真正改善了问题,最后要看这些指标,而不是看代码结构。

8. 一次可以照用的重构收尾清单

文章最后列一份可以直接保存的重构收尾清单,适合在类似 zhshop 的商城项目上线前逐项确认。

  1. 所有重构分支已合并,提交信息可追溯。
  2. 数据库字段迁移脚本、回填脚本、回滚脚本已评审。
  3. 新增字段和旧字段已在测试环境做一致性校验。
  4. 业务回归清单执行完毕,覆盖正常流程和异常分支。
  5. 接口兼容性冒烟通过,返回字段无破坏性变更。
  6. 并发压测完成,库存扣减无超卖,订单状态无乱序。
  7. Redis、数据库连接池等基础组件具备快速失败能力。
  8. 日志关键字统一,搜索orderNo能串联订单全链路。
  9. 发布回滚方案已写好,并经过至少一次演练。
  10. 代码评审清单无未处理项。

重构完成并不意味着项目从此没有技术债。它更像是一次系统性整理,让后续的修改有了明确边界。真正重要的不是重构这一动作,而是重构之后,团队每次改代码时都能说清楚影响面。把 zhshop 这次重构当作一个中途检查点,后续再遇到性能问题、流量增长或新业务接入,就可以在更稳定的结构上继续演进。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 14:18:41

Ice:3 招搞定 Mac 菜单栏图标收纳,刘海屏不再挡住 Wi-Fi

Ice&#xff1a;3 招搞定 Mac 菜单栏图标收纳&#xff0c;刘海屏不再挡住 Wi-Fi 【免费下载链接】Ice Powerful menu bar manager for macOS 项目地址: https://gitcode.com/GitHub_Trending/ice/Ice Mac 菜单栏越装越挤&#xff0c;Wi-Fi、电池被挤进角落&#xff0c;刘…

作者头像 李华
网站建设 2026/9/4 14:13:31

SpringBoot+Vue全栈实战:个人财务管理系统设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 14:12:50

【单片机毕业设计】多档位可调语音控制智能台灯硬件系统设计 基于单片机的自动调光人体感应智能台灯实现(021406)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/4 14:11:17

OpenScreen 导出失败快速修复:MP4 与 GIF 导不出的 3 个主因

OpenScreen 导出失败快速修复&#xff1a;MP4 与 GIF 导不出的 3 个主因 【免费下载链接】openscreen Create stunning demos for free. Open-source, no subscriptions, no watermarks, and free for commercial use. An alternative to Screen Studio. 项目地址: https://g…

作者头像 李华
网站建设 2026/9/4 14:09:55

一份配色,适配 20+ 终端:iTerm2-Color-Schemes 完整使用指南

一份配色&#xff0c;适配 20 终端&#xff1a;iTerm2-Color-Schemes 完整使用指南 【免费下载链接】iTerm2-Color-Schemes Over 450 terminal color schemes/themes for iTerm/iTerm2. Includes ports to Terminal, Konsole, PuTTY, Xresources, XRDB, Remmina, Termite, XFCE…

作者头像 李华