分布式事务解决方案:一次电商平台重构实战记录
我是李工,一个在后端开发领域摸爬滚打了5年的老码农。今天想和大家聊聊我在一次电商平台重构过程中踩过的“分布式事务”坑。如果你也遇到过服务拆分之后订单、支付、库存之间数据一致性的问题,那这篇文章应该能给你一些启发。
项目背景:从单体架构到微服务的阵痛期

去年我所在的电商公司开始做系统架构升级,目标是把原来的大单体系统拆分成订单中心、商品中心、库存中心、交易中心等多个微服务模块。整体采用SpringCloud Alibaba技术栈,MySQL作为主要数据库,RocketMQ用于异步通信,Seata准备用来解决跨服务的数据一致性问题。
最核心的一次业务流程就是下单,涉及三个关键操作:
- 扣减库存(InventoryService)
- 创建订单(OrderService)
- 冻结用户余额(PayService)
这三个操作分别属于不同的服务模块,并且每一步都涉及到各自的数据写入。一旦出现部分执行成功、部分失败的情况,后果会很严重——比如订单创建了但库存没扣减,或者库存扣减了但余额没有冻结,这些都会导致资损或超卖问题。
挑战一:传统本地事务行不通

我们最开始尝试沿用传统的本地事务机制,也就是每个服务单独提交自己的事务。举个例子,在订单服务里这样处理:
@Transactional
public void createOrder(...) {
// 调用库存接口预扣库存
inventoryFeignClient.reduceStock(productId, quantity);
// 创建订单主表
orderDao.insert(order);
// 调用支付接口冻结余额
payFeignClient.freezeAmount(userId, amount);
}
这种方式最大的问题是无法保证全局一致性。假设调用支付服务时失败了,前面已经完成的操作就变成了脏数据。即使使用try-catch回滚,也无法回退远程服务已经执行成功的部分。
当时我们在压测环境发现了一个特别离谱的问题:某个测试账号余额为0,但竟然完成了下单操作,原因是payService调用失败后,异常被吞掉了。最终系统中出现了无效订单和未扣减库存,只能人工介入处理。
这显然是不能接受的。
解决方案:引入 Seata 实现 TCC 型分布式事务

我们经过调研和讨论,决定采用 Seata 来实现基于 TCC 模式的分布式事务管理。这里说句实在话,TCC 对业务侵入性比较强,但对于我们的业务场景来说还是可以接受的。
架构设计思路
整个事务流程分为两个阶段:
- Try 阶段(资源预留)
- 检查可用性但不真正修改数据(如判断库存是否足够)
- 预留状态标记(如冻结库存数量)
- Confirm / Cancel 阶段(提交或回滚)
- Confirm:执行实际变更(如正式扣减库存)
- Cancel:撤销 Try 中的操作(如解冻库存)
以库存服务为例,它需要提供如下接口:
// Try阶段
boolean prepareReduceStock(Long productId, Integer quantity);
// Confirm阶段
void confirmReduceStock();
// Cancel阶段
void cancelReduceStock();
订单服务作为发起方,通过 Feign 调用其他服务的 Try 方法完成预检查和资源锁定,在所有 Try 成功后再统一触发 Confirm;若某一步失败,则触发所有参与者的 Cancel 操作进行回滚。
关键配置与代码片段
首先在各个服务中添加 Seata 的相关依赖:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.6.1</version>
</dependency>
然后配置 Seata 客户端配置文件application.yml:
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
grouplist:
default: seata-server:8091

订单服务的核心事务入口示例:
@GlobalTransactional(rollbackFor = Exception.class)
public OrderDTO createOrder(CreateOrderRequest request) {
try {
// 调用库存 Try 接口
inventoryService.prepareReduceStock(request.getProductId(), request.getQuantity());
// 创建订单(本地事务)
Order order = saveOrderLocal(request);
// 调用支付 Try 接口
payService.prepareFreezeAccount(request.getUserId(), request.getAmount());
// 提交事务,Seata自动处理后续Confirm或Cancel
return convertToDto(order);
} catch (Exception e) {
log.error("下单失败", e);
throw new BizException("下单失败");
}
}
各服务需实现 TCC 接口:
@Component
public class InventoryTccAction {
@Autowired
private InventoryMapper inventoryMapper;
@TwoPhaseBusinessAction(name = "prepareReduceStock")
public boolean prepareReduceStock(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity) {
// 减少可用库存,增加冻结库存
return inventoryMapper.updateStock(productId, quantity) > 0;
}
@Commit
public boolean confirmReduceStock(BusinessActionContext ctx) {
Long productId = (Long) ctx.getActionContext("productId");
Integer quantity = (Integer) ctx.getActionContext("quantity");
// 真正扣减库存
return inventoryMapper.confirmStock(productId, quantity) > 0;
}
@Rollback
public boolean cancelReduceStock(BusinessActionContext ctx) {
Long productId = (Long) ctx.getActionContext("productId");
Integer quantity = (Integer) ctx.getActionContext("quantity");
// 回退冻结库存
return inventoryMapper.rollbackStock(productId, quantity) > 0;
}
}
踩坑经历:生产环境的那些事儿
说实话,部署上线之后并没有立刻顺利跑通,我们遇到了几个典型问题:
1. 网络超时导致 Xid 丢失
在高峰期时,由于 Feign 调用默认超时时间较短(1s),导致部分请求在 Try 阶段就超时。这种情况下 Seata 无法感知事务状态,容易造成悬挂事务(Dangling Transactions)。
解决办法:
增加 Feign + Ribbon 的超时设置:
ribbon: ReadTimeout: 3000 ConnectTimeout: 3000 feign: client: config: default: http-read-timeout: 3000在网关层面设置合理的超时重试策略(不要盲目重试)
2. Cancle 操作幂等性问题
我们初期的取消逻辑没有考虑重复执行的问题。例如,Cancel 被执行了两次,结果将原本应保留的库存又多减了一次。
修复方案:
- 引入唯一事务ID(branchId)到 Cancel 方法中
- 在服务端维护一个 Cancel 日志表,记录已执行的 Cancel 操作,执行前先校验是否已经处理过该 branchId
3. Seata Server 性能瓶颈
随着系统访问量的增加,Seata Server 单节点扛不住压力。特别是在大促期间,经常出现 Global Lock 获取失败的情况。
我们最终采用了以下优化措施:
- 使用 DB 模式持久化事务日志,而不是默认的 file 模式
- 增加多个 Seata Server 实例,负载均衡接入
- 做了定制开发:对非高一致性场景放宽锁竞争策略
效果总结:系统稳定性显著提升
上线 TCC 分布式事务方案后,系统整体表现非常稳定。从监控数据来看:
- 下单交易成功率由原来的 97% 提升至 99.95%
- 每天因分布式异常导致的人工干预次数基本归零
- 最终一致性得到了保障,客户投诉率下降明显
当然,代价也不是没有。比如 TCC 增加了代码复杂度,每个多资源操作都需要编写 Try、Confirm、Cancel 三个接口。此外,对运维人员也提出了更高的要求——必须熟悉 Seata 的日志结构、事务恢复机制等。
给读者的一些经验分享
如果你也在考虑使用 Seata 或者其他的分布式事务框架,以下几个建议或许对你有用:
✅ 优先考虑本地事务 + 最终一致性方案
并不是所有场景都适合使用强一致的分布式事务。比如某些日志类数据、非敏感金额类字段,可以通过 RocketMQ + 补偿任务来实现最终一致性,反而更轻量高效。
🛠️ 提前做好事务隔离级别设计
不同数据库的默认隔离级别可能影响事务行为。建议统一设置为 Read Committed 并配合 SELECT FOR UPDATE 显式加锁。
🔐 注意日志安全与敏感信息脱敏
Seata 会在事务日志中记录业务参数,务必注意隐私数据的脱敏处理,避免泄露风险。
🧪 加强压测与回滚演练
每次上线前我们都会模拟各类网络故障和节点宕机,测试 Cancel 是否能正确执行、数据是否可恢复。建议你也这样做。
📈 结合监控平台做实时追踪
我们将 Seata 的 TM、RM、TC 监控指标接入了 Prometheus + Grafana,实现了对分布式事务状态的可视化管理,非常推荐这套组合。
分布式事务从来不是一个轻松的话题,它关乎系统的健壮性和用户体验。通过这次项目的磨练,我也深刻体会到,“稳”的背后往往是一点一点细节打磨出来的。希望我的经验能帮你在面对类似问题时少走些弯路。如果你也遇到过这类问题,欢迎留言交流!

评论 0