如何保证下单成功?揭秘SHOPTNT基于命令模式的分布式事务解决方案

在库存扣减、优惠券核销、积分抵扣的复杂场景下,订单创建如何保证要么全成功,要么全回滚?我们放弃了TCC和Seata,选择了更轻量级的设计。

大家好,我是开源B2B2C商城项目SHOPTNT的开发者。电商系统的“下单”流程,是典型的分布式事务场景:它需要调用库存、优惠券、积分、订单等多个服务,这些操作必须保持原子性——要么全部成功,要么全部失败。

传统的“TCC”、“2PC”或引入“Seata”等方案虽然成熟,但往往较重。在SHOPTNT中,我们采用了一种基于 “命令模式(Command Pattern)” 的轻量级解决方案,巧妙地在业务层实现了最终一致性。今天,我将深度解析这套设计的核心思想与实现。

一、 核心挑战:下单的“原子性”难题

用户点击“下单”后,系统需要完成一系列操作:

  1. 锁库存:扣减商品库存,防止超卖。

  2. 核销优惠券:将用户使用的优惠券标记为“已使用”。

  3. 扣减积分:扣除用户支付的积分。

  4. 创建订单:将订单数据持久化到数据库。

这些操作分散在不同的服务和数据库中。任何一个步骤失败,都必须能够自动、干净地回滚之前所有已成功的操作,否则就会导致数据不一致(如库存扣了但订单没生成)。

二、 架构核心:命令模式与人工事务日志

我们的解决方案的核心思想是:将每个操作封装成一个独立的“命令”(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)
这是最关键的命令,它的executerollback都必须保证幂等性。

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);
    }
}
五、 方案优势与权衡

优势:

  1. 轻量级:无需引入沉重的分布式事务框架,复杂度可控。

  2. 灵活性:每个服务的补偿逻辑可以非常定制化,符合业务需求。

  3. 清晰性:代码结构清晰,可读性强,问题易于排查。每个命令的职责单一。

  4. 最终一致性保证:通过可靠的“回滚”机制,保证了数据的最终一致性。

需要注意的权衡(Trade-off):

  1. 非强一致性:在execute成功到rollback被调用的极短时间窗口内,数据是处于中间状态的(如库存已扣减但订单可能最终失败)。这对于电商场景是可接受的。

  2. 回滚失败的处理:需要保证rollback方法本身的可靠性。如果回滚也失败,需要记录异常日志并触发告警,由人工介入处理。这是一种面向失败的设计(Design for Failure)


邀请与互动

在分布式系统中,放弃强一致性,拥抱最终一致性,并设计好补偿机制,是一种务实而高效的架构哲学。欢迎你来我们的开源项目深入探讨:

https://gitee.com/bbc-se/shoptnt

你可以:

  • ⭐ Star/Fork 项目,亲自查看TradeCreatorImpl和各个Command的完整实现,理解命令模式的精妙用法。

  • 📖 阅读文档,了解我们如何处理更边缘的异常情况。

  • 🐛 在Issue中讨论:你对分布式事务有什么见解?你会如何设计回滚机制?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值