在库存扣减、优惠券核销、积分抵扣的复杂场景下,订单创建如何保证要么全成功,要么全回滚?我们放弃了TCC和Seata,选择了更轻量级的设计。
大家好,我是开源B2B2C商城项目SHOPTNT的开发者。电商系统的“下单”流程,是典型的分布式事务场景:它需要调用库存、优惠券、积分、订单等多个服务,这些操作必须保持原子性——要么全部成功,要么全部失败。
传统的“TCC”、“2PC”或引入“Seata”等方案虽然成熟,但往往较重。在SHOPTNT中,我们采用了一种基于 “命令模式(Command Pattern)” 的轻量级解决方案,巧妙地在业务层实现了最终一致性。今天,我将深度解析这套设计的核心思想与实现。
一、 核心挑战:下单的“原子性”难题
用户点击“下单”后,系统需要完成一系列操作:
-
锁库存:扣减商品库存,防止超卖。
-
核销优惠券:将用户使用的优惠券标记为“已使用”。
-
扣减积分:扣除用户支付的积分。
-
创建订单:将订单数据持久化到数据库。
这些操作分散在不同的服务和数据库中。任何一个步骤失败,都必须能够自动、干净地回滚之前所有已成功的操作,否则就会导致数据不一致(如库存扣了但订单没生成)。
二、 架构核心:命令模式与人工事务日志
我们的解决方案的核心思想是:将每个操作封装成一个独立的“命令”(Command)对象,并显式地为其实现“执行(execute)”和“回滚(rollback)”方法。
整个下单流程的架构如下图所示:
text
[TradeCreatorImpl.create()] (事务边界) | v [CommandFactory] -> 构建命令列表 (按定义好的顺序) | v [循环执行命令] -> command.execute() |-> 成功:记录该命令到successCommands列表 |-> 失败:立即停止,并循环回滚successCommands列表中的每一个命令(command.rollback()) | v [全部成功] -> 发送消息,完成下单
1. 总控中心:TradeCreatorImpl
TradeCreatorImpl
是分布式事务的“协调者”,它控制着整个命令流程的执行与回滚。
java
// TradeCreatorImpl.java (精简核心逻辑) @Transactional(rollbackFor = Exception.class) // 声明式事务,仅用于订单数据本身 public void create(TradeVO tradeVO) { List<OrderCreateCommand> successCommands = new ArrayList<>(); // 成功命令日志 List<OrderCreateCommand> allCommands = commandFactory.buildTradeCreateCommands(tradeVO); // 执行所有命令,并记录成功的 for (OrderCreateCommand command : allCommands) { CommandResult result = command.execute(); if (result.getResult()) { successCommands.add(command); // 记录成功日志 } else { // 【关键】遇到失败,立即回滚之前所有成功的命令 rollbackSuccessCommands(successCommands); throw new ServiceException("下单失败: " + result.getErrorMessage()); } } // ... 后续处理(如缓存交易数据、发送消息) } // 回滚方法:逆向执行所有成功命令的rollback操作 private void rollbackSuccessCommands(List<OrderCreateCommand> successCommands) { for (OrderCreateCommand cmd : successCommands) { cmd.rollback(); // 调用每个命令自己的回滚逻辑 } }
设计亮点:
-
人工事务日志:
successCommands
列表扮演了“事务日志”的角色,清晰记录了哪些操作需要补偿。 -
事务边界清晰:虽然每个
command.execute()
可能涉及远程调用,但数据库事务的边界很清晰,只覆盖本地订单库的写入,避免了分布式事务的复杂性。
三、 命令工厂:CommandFactory
—— 秩序的维护者
命令的执行顺序至关重要。例如,必须先创建订单记录,才能去核销优惠券(因为核销需要订单SN)。CommandFactory
负责以正确的顺序组装命令。
java
// CommandFactory.java public List<OrderCreateCommand> buildOrderCreateCommands(OrderDTO orderDTO) { List<OrderCreateCommand> commandList = new ArrayList<>(); // 1. 积分扣减 commandList.add(new MemberPointDeductCommand(orderDTO)); // 2. 订单写库 (必须先有订单号,后续操作才能关联) commandList.add(new OrderWriteDatabaseCommand(orderDTO)); // 3. 店铺优惠券核销 (需要订单SN) commandList.add(new ShopCouponUseCommand(orderDTO)); // 4. 库存扣减 (应最后操作,因为库存是核心资源) commandList.add(new StockDeductCommand(orderDTO)); // 对命令按预定义的顺序(sequence())进行排序 commandList.sort(Comparator.comparing(OrderCreateCommand::sequence)); return commandList; }
设计亮点:
-
顺序可控:通过
sequence()
方法明确定义每个命令的执行顺序,避免业务逻辑错误。 -
依赖注入:命令对象是
new
出来的,但通过beanFactory.autowireBean(c)
为其注入所需的Spring Bean,巧妙解决了“有状态命令”与“Spring单例”的矛盾。
四、 实战命令解析:以库存扣减和优惠券为例
1. 库存扣减命令 (StockDeductCommand
)
这是最关键的命令,它的execute
和rollback
都必须保证幂等性。
java
public class StockDeductCommand implements OrderCreateCommand { @Autowired private GoodsQuantityClient goodsQuantityClient; // 库存服务客户端 @Override public CommandResult execute() { // 构建扣减请求(数量为负数) List<GoodsQuantityVO> deductList = buildDeductList(order.getOrderSkuList()); // 调用库存服务的扣减API(内部通常用Lua脚本保证原子性) boolean success = goodsQuantityClient.updateSkuQuantity(deductList); return new CommandResult(success, success ? "成功" : "库存不足"); } @Override public void rollback() { // 构建恢复请求(数量为正数,加回去) List<GoodsQuantityVO> recoverList = buildRecoverList(order.getOrderSkuList()); goodsQuantityClient.updateSkuQuantity(recoverList); // 调用相同的API } }
-
回滚的本质:就是执行一个反向操作。扣减(-n)的回滚是增加(+n)。
2. 优惠券核销命令 (ShopCouponUseCommand
)
java
public class ShopCouponUseCommand implements OrderCreateCommand { @Override public CommandResult execute() { // 调用优惠券服务,将券状态置为“已使用”,并绑定订单SN boolean success = memberCouponClient.updateMemberUsedCoupon( orderDTO.getMemberId(), mcIds, orderDTO.getSn()); return new CommandResult(success, "优惠券核销失败"); } @Override public void rollback() { // 回滚:调用优惠券服务,将券状态恢复为“未使用”,并解绑订单SN memberCouponClient.cancelUseCoupons(orderDTO.getMemberId(), mcIds); } }
五、 方案优势与权衡
优势:
-
轻量级:无需引入沉重的分布式事务框架,复杂度可控。
-
灵活性:每个服务的补偿逻辑可以非常定制化,符合业务需求。
-
清晰性:代码结构清晰,可读性强,问题易于排查。每个命令的职责单一。
-
最终一致性保证:通过可靠的“回滚”机制,保证了数据的最终一致性。
需要注意的权衡(Trade-off):
-
非强一致性:在
execute
成功到rollback
被调用的极短时间窗口内,数据是处于中间状态的(如库存已扣减但订单可能最终失败)。这对于电商场景是可接受的。 -
回滚失败的处理:需要保证
rollback
方法本身的可靠性。如果回滚也失败,需要记录异常日志并触发告警,由人工介入处理。这是一种面向失败的设计(Design for Failure)。
邀请与互动
在分布式系统中,放弃强一致性,拥抱最终一致性,并设计好补偿机制,是一种务实而高效的架构哲学。欢迎你来我们的开源项目深入探讨:
https://gitee.com/bbc-se/shoptnt
你可以:
-
⭐ Star/Fork 项目,亲自查看
TradeCreatorImpl
和各个Command
的完整实现,理解命令模式的精妙用法。 -
📖 阅读文档,了解我们如何处理更边缘的异常情况。
-
🐛 在Issue中讨论:你对分布式事务有什么见解?你会如何设计回滚机制?