系统没拆开之前,下单这件事很简单。
一个数据库事务里,创建订单、扣库存、核销优惠券。提交成功就是成功,回滚失败就是失败,边界很清楚。
后来系统拆开了。订单是订单服务,库存是库存服务,优惠券是营销服务,支付回调又来自外部系统。一次下单跨了多个服务、多个数据库、多个消息。
于是问题来了:订单创建成功了,库存扣了,优惠券核销失败怎么办?支付成功了,积分重复到账怎么办?消息发出去了,消费者处理到一半挂了怎么办?
系统越拆,正确性越贵。
一致性问题最磨人的地方,是它经常不是马上错。
用户下单成功了,库存稍后才扣;支付回调到了,订单状态还没更新;积分发放失败了,但主流程已经走完。单看每一步都能解释,串起来就变成了“为什么用户看到的和后台看到的不一样”。
这种问题一旦进了客服和运营群,技术解释通常很难听。你说异步、重试、最终一致,对方关心的是:这笔订单到底算不算成功,用户要不要补偿,数据以后还能不能信。
所以分布式一致性不是只在讨论 CAP,它最后会落到业务可信度上。
订单、库存、支付、积分只要拆开,就要提前想清楚失败、重试和补偿怎么把结果收回来。
一、不要一上来就追求强一致
分布式事务可以追求强一致,但代价很高。
两阶段提交、XA 这类方案能让多个资源一起提交或回滚,但会带来锁持有时间长、性能下降、可用性受影响、实现复杂等问题。
很多业务真正需要的,不是每一秒都绝对一致,而是最终能回到正确状态。
比如订单已创建、积分稍后到账;支付成功、通知稍后补发;库存预扣成功、超时未支付再释放。
最终一致不是“不一致也没关系”,而是允许短时间处于中间状态,但必须有机制把状态收敛回来。
二、TCC 和 Saga 不是越复杂越好
TCC 是 Try、Confirm、Cancel。
它适合资源预留明确、业务状态可控的场景。比如库存先冻结,支付成功后确认扣减,超时或失败后取消冻结。
TCC 的控制力强,但业务侵入也强。每个参与方都要实现 Try、Confirm、Cancel,还要处理幂等、空回滚、悬挂等边界情况。
Saga 更像一组本地事务加补偿动作。
比如创建订单、扣优惠券、发积分。如果后面失败,就执行反向补偿。它适合长流程业务,但补偿不一定能完全恢复原状。
选择时要回到业务:
- 资源预留明确,正确性要求高,可以考虑 TCC。
- 流程较长,允许补偿,可以考虑 Saga。
- 只是异步通知或衍生数据,用消息最终一致通常更简单。
三、本地消息表和 Outbox 很朴素,但很有用
很多一致性问题卡在一句话:数据库写成功了,但消息没发出去。
比如订单创建成功后要发订单创建事件。如果数据库提交成功,但 MQ 发送失败,下游永远不知道这件事。
本地消息表和 Outbox 的思路是,把业务数据和待发送消息写入同一个本地事务。
本地事务:
写订单
写 outbox 消息
后台任务:
扫描未发送消息
投递 MQ
成功后标记已发送这样至少能保证:只要订单写成功,消息就一定有记录,后面可以重试投递。
它不花哨,但工程上很稳。
四、分布式锁只能解决一部分问题
分布式锁经常被拿来解决并发问题,但它不是一致性的万能答案。
它适合控制同一资源在同一时间只有一个操作,比如防止重复提交、热点资源更新、定时任务多实例抢占。
但分布式锁也有风险:锁超时、锁误删、业务执行时间超过锁租约、网络抖动、锁服务不可用。
所以锁要有超时时间,释放锁要校验持有者,业务要能幂等。更重要的是,不要把所有一致性问题都交给锁。
很多时候,状态机、唯一约束、版本号、幂等表,比一把锁更可靠。
五、幂等是最终一致的底座
只要系统里有重试,就必须有幂等。
消息会重复投递,接口会重复调用,补偿会重复执行。没有幂等,重试就可能把系统越修越坏。
常见幂等方式包括:
- 业务唯一号。
- 唯一索引。
- 状态机流转。
- 版本号或乐观锁。
- 幂等记录表。
我更喜欢用业务状态做约束。比如订单只能从“待支付”变成“已支付”,不能从“已关闭”再变成“已支付”。状态机设计清楚,很多异常请求自然会被挡住。
六、可以带走的判断清单
- 先判断业务是否真的需要强一致。
- 能接受短暂中间状态,就优先考虑最终一致。
- TCC 适合明确资源预留,但业务侵入强。
- Saga 适合长流程,但补偿要设计清楚。
- 业务数据和消息要可靠关联,可以考虑 Outbox。
- 有重试就必须有幂等。
- 状态机和唯一约束经常比一把分布式锁更稳。
分布式一致性不是追求理论完美,而是让系统在失败、重试和异步中还能回到正确状态。
本系列:
暂无评论