问题从哪里来
单体架构下,下单扣库存是一个本地事务,数据库帮我们兜底。拆成微服务之后,订单服务和库存服务各自持有数据库,同一个业务操作跨越了两个事务边界——这就是分布式事务问题的全部来源。
值得强调的是:这个问题是架构拆分带来的成本,不是收益。所以第一个该问的问题永远是"这两个服务真的需要拆开吗",而不是"用哪个分布式事务框架"。
四类方案与它们的代价
1. 2PC / XA
数据库层面的两阶段提交。优点是对业务透明,缺点是在准备阶段会长时间持有锁。在大促场景下,热点商品的库存行会成为绝对瓶颈。
适用:低并发、强一致要求的金融类场景。 不适用:电商交易主链路。
2. TCC(Try-Confirm-Cancel)
业务层面的两阶段:Try 阶段预留资源,Confirm 提交,Cancel 回滚。库存场景下 Try 就是"冻结库存"。
一致性好,性能可控,代价是每个参与方都要实现三个接口,且必须处理空回滚、悬挂、幂等三类边界情况。开发成本大约是普通接口的三倍。
适用:资金、库存等对准确性要求极高且并发中等的场景。
3. 本地消息表 + 最终一致
业务操作与消息写入在同一个本地事务中完成,后台任务轮询未投递的消息并推送。下游消费失败则重试,重试到一定次数进入死信队列人工介入。
实现简单、性能好,代价是存在短暂的不一致窗口。
适用:绝大多数电商场景。这也是我们在主链路上的默认选择。
4. Saga
一长串本地事务 + 对应的补偿操作,任一步失败则反向补偿。适合流程长、参与方多的业务,比如跨境订单的报关、清关、配送链路。
代价是补偿逻辑本身可能失败,需要设计补偿的补偿,复杂度会指数上升。
我们的选择依据
在实际项目中,我们的决策顺序是这样的:
第一步,能不能不拆。 强一致要求高且总在一起变化的两个能力,放在同一个服务里是最省事的方案。库存和订单在很多中小规模系统里完全没必要拆。
第二步,能不能容忍最终一致。 如果业务能接受几秒钟的不一致(大多数场景都能),本地消息表就是性价比最高的答案。
第三步,确实需要强一致再上 TCC。 并且严格限制范围,只在核心的资金与库存扣减上使用。
一个真实的坑
某次大促前的压测中,我们发现库存扣减的 TPS 上不去。排查发现是所有请求都在争抢同一个热点 SKU 的数据库行锁。
最终的解法不是换分布式事务方案,而是库存分桶:把一个 SKU 的 1000 件库存拆成 10 个桶各 100 件,请求按用户 ID 哈希路由到不同桶,锁竞争下降一个数量级。桶内余量不足时再向其他桶借调。
这个案例说明的道理是:一致性问题的瓶颈,往往不在一致性协议本身,而在数据模型。换框架之前,先看看数据能不能拆。
必须做的三件事
无论选哪种方案,这三件事都不能省:
- 幂等。每个写接口都要有业务幂等键,重复调用必须返回相同结果。
- 可观测。未完成的分布式操作要有明确的存量统计与时长分布,异常积压能第一时间发现。
- 对账。定时任务比对上下游数据,差异自动告警。再好的方案也会有漏网之鱼,对账是最后一道防线。
工程上没有完美的一致性方案,只有与业务容忍度匹配的取舍。