ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

中小型商城系统重构:状态机与数据库平滑迁移实践 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 的反面教材是早期曾经在重构商品分类时顺手改了商品查询接口的返回字段导致管理后台和客户端同时报错最后用一整个晚上回滚排查。问题不是出在代码质量而是出在“没有区分内部改动和外部影响”。兼容红线至少包含下面几项数据库存量数据和字段语义。除非有明确的迁移脚本和回滚方案否则不要删除旧字段。对外接口的 URL、请求参数、返回结构。已有的 App、小程序、管理后台都是接口消费方不能按内部设计自由改动。权限模型的角色编码和权限粒度。如果旧系统使用admin、operator等编码最少在第一个版本保留旧编码兼容。消息和异步任务的格式。订单创建后的 MQ 消息、延迟取消任务、售后提醒如果换了消息体消费者可能反序列化失败。日志和监控字段。线上排障时很多团队已经习惯按orderNo、userId搜索日志重构时不要随意换字段名。确定红线之后所有重构任务都要做一道判断题这个改动是新增还是修改是内部改造还是外部影响。外部影响优先向后兼容内部改造才允许放开手脚。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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version11/java.version mybatis.spring.boot.version2.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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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.activelocal启动后端最后再启动前端。前端开发服务器和后端联调时要注意反向代理配置避免跨域问题掩盖真实接口错误。3. 核心重构订单状态机、库存幂等和支付回调3.1 按业务模块拆包而不是按技术层拆包早期的 zhshop 结构性问题是“先按技术分层再额外堆模块”。典型表现是controller、service、mapper下面放着所有业务的类。这种做法在演示项目里没有问题但在真实商城项目里会导致OrderController和StockController可以毫无阻碍地互相访问私有方法。重构后的包结构建议按业务模块划分每个模块内再保留自己的控制层、应用层和数据访问层。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 MapOrderStatus, MapOrderEvent, 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) { MapOrderEvent, 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); }这里不要使用先get再del的解锁方式因为如果锁已经过期并被其他线程持有当前线程会把别人的锁删掉。使用 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 MapPayChannel, PayChannelHandler handlerMap; public PayNotifyDispatcher(ListPayChannelHandler 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 版本ListPayChannelHandler的注入方式同样可用不需要让每个实现类自己注册到 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 代理注解不会生效。结果就是库存扣减失败后下单流程可能仍然提交了部分数据。解决方法有三种把deductStock放到独立的StockService类中由 Spring 代理调用。在createOrder方法上增加完整事务将库存扣减和订单创建合入同一事务。如果必须使用自调用可以注入ApplicationContext获取代理对象但推荐重构掉这种写法。代码评审时事务方法自调用是优先级最高的检查项因为它在编译期不会暴露问题只在业务异常时表现为数据不一致。6.3 旧字段和新字段不一致历史数据查询混乱迁移期间status和order_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 脚本并携带请求唯一标识。数据库迁移脚本是否有回滚脚本。状态机是否覆盖所有历史状态和异常事件。日志是否包含orderNo、userId、渠道等关键信息。本地启动配置是否包含环境差异是否误提交密码和密钥。评审清单不用追求数量重点是让每位开发者知道重构后的代码该按什么标准检查。7.2 发布与回滚重构后的发布要采用“小步发布”策略而不是把一周改动一次性上线。即便是在单体项目里也可以通过配置开关实现灰度。例如订单模块可以拆成“状态机逻辑”和“新状态字段”两个开关先打开字段迁移脚本再切换业务读路径最后再启用状态机。回滚方案必须在发布前写清楚。比较安全的顺序是回滚代码到上一个可发布版本。关闭新状态字段的读取开关。保留新增字段不删除数据库列。确认旧逻辑恢复后再分析失败原因。不要在线上直接执行DROP COLUMN回滚数据库回滚永远慢于代码回滚。数据字段宁可保留一段时间也不要因为清理字段造成不可逆损失。7.3 后续演进方向zhshop 这类商城系统完成第一轮重构后后续可以继续在几个方向做结构化改造。一是把订单模块中的支付、库存、物流等交互收敛到领域事件。比如“订单已支付”事件由订单模块发出库存、积分、消息通知模块订阅。这样可以进一步降低模块间的同步等待。二是引入开放的 API 规范文档将接口契约和代码同步管理。商城系统的前端、App、小程序都在变而接口是稳定契约把契约版本化能减少沟通成本。三是为高频查询建立可观测指标。商品详情、购物车、订单列表都是高流量接口至少统计 P99 响应时间、慢查询数、缓存命中率和异常率。重构是否真正改善了问题最后要看这些指标而不是看代码结构。8. 一次可以照用的重构收尾清单文章最后列一份可以直接保存的重构收尾清单适合在类似 zhshop 的商城项目上线前逐项确认。所有重构分支已合并提交信息可追溯。数据库字段迁移脚本、回填脚本、回滚脚本已评审。新增字段和旧字段已在测试环境做一致性校验。业务回归清单执行完毕覆盖正常流程和异常分支。接口兼容性冒烟通过返回字段无破坏性变更。并发压测完成库存扣减无超卖订单状态无乱序。Redis、数据库连接池等基础组件具备快速失败能力。日志关键字统一搜索orderNo能串联订单全链路。发布回滚方案已写好并经过至少一次演练。代码评审清单无未处理项。重构完成并不意味着项目从此没有技术债。它更像是一次系统性整理让后续的修改有了明确边界。真正重要的不是重构这一动作而是重构之后团队每次改代码时都能说清楚影响面。把 zhshop 这次重构当作一个中途检查点后续再遇到性能问题、流量增长或新业务接入就可以在更稳定的结构上继续演进。
返回列表