📌关键词:分布式事务、分库分表、数据一致性、TCC 、消息队列
大家好呀!我是数据库小学妹👋
前面我们学了分库分表,把数据拆到了多个数据库里,突破了单库的存储和性能瓶颈。但随之而来的一个棘手问题是:跨库操作时,怎么保证多个库的数据同时成功或同时失败?
比如下单场景:
- 订单库扣减订单金额
- 库存库减少商品库存
这两个操作分属不同的数据库。如果订单库扣款成功,但库存库更新失败(网络超时、宕机等),就会出现钱扣了,货没减的严重问题。
这就是分布式事务要解决的难题。今天我就把自己学到的分布式事务方案分享出来,帮你理解如何在分布式环境下保证数据一致性。
一、本地事务 vs 分布式事务:单打独斗 vs 团队协作
| 维度 | 本地事务 (单库) | 分布式事务 (多库/微服务) |
|---|---|---|
| 控制范围 | 单个数据库实例 | 跨越多个数据库、多个服务 |
| 实现机制 | 依靠数据库的 ACID 特性 | 依赖协调者 (Coordinator) 和协议 |
| 性能表现 | 极高 (无网络开销) | 较低 (涉及网络通信和锁竞争) |
| 一致性 | 强一致性 | 强一致性 或 最终一致性 |
一句话总结:单库事务靠数据库,跨库事务靠“协议”和“补偿机制”。
二、分布式事务的常见解决方案
| 方案 | 核心原理 | 适用场景 | 评价 |
|---|---|---|---|
| 两阶段提交(2PC/XA) | 准备阶段(询问各参与者能否提交)→ 提交/回滚阶段 | 银行、金融等强一致性要求极高的场景 | 过时/慎用:性能差,锁粒度大 |
| TCC(Try-Confirm-Cancel) | Try预留资源 → Confirm确认执行 → Cancel回滚 | 复杂业务场景,如预订机票、酒店 | 推荐:性能好,但开发成本高 |
| 最终一致性(消息队列) | 主业务先执行,发消息;从业务消费消息执行 | 订单、积分、日志等可接受短暂延迟的场景 | 首选:互联网高并发场景的标配 |
| Seata(阿里开源) | AT模式自动回滚(基于数据快照) | 微服务架构中较常用 | 推荐:代码侵入低,适合Spring Cloud生态 |
💡 对于大部分互联网业务,最终一致性 + 消息队列已经足够。强一致性需求(如转账)才考虑TCC或2PC。
三、 实战演示:用消息队列实现最终一致性
场景:用户下单 -> 扣库存 -> 创建订单 -> 扣积分
流程逻辑:
- 本地事务:订单服务开启事务,创建订单(状态=待支付),并同步写入一条“扣减库存消息”到MQ。
- 消费处理:库存服务消费MQ消息,执行扣库存。
- 状态更新:库存扣减成功,发送回调或直接更新订单状态。
- 异常补偿:如果MQ消费失败,利用死信队列(DLQ)进行告警或人工介入。
关键点(避坑必看):
- 幂等性:MQ可能重复投递,扣库存接口必须支持幂等(如通过Redis记录已处理的消息ID)。
- 本地消息表:为了保证“写库”和“发MQ”的原子性,通常需要在业务库中建一张
message_queue表,通过定时任务扫描发送,避免事务回滚导致消息发了但业务没做。
四、 避坑指南:分布式事务的“深水区”
- 陷阱:消息丢失
- 现象:订单创建了,库存没扣,钱也没退。
- 对策:MQ开启持久化 + 生产者Confirm机制。
- 陷阱:重复消费
- 现象:同一笔订单被扣了两次库存。
- 对策:消费端做幂等设计(数据库唯一索引/Redis Token机制)。
- 陷阱:悬挂问题 (TCC特有)
- 现象:Cancel比Try先执行。
- 对策:Try接口需要记录事务状态,Cancel接收到时先检查Try是否已执行。
小学妹总结:分布式事务是把“双刃剑”。首选策略通常是:通过合理的数据分片,尽量让相关数据落在同一个库(单机事务),实在不行再用分布式事务兜底。
五、总结
- 分库分表后必选:跨库操作必须引入分布式事务机制,否则数据会乱。
- 首选异步:互联网业务优先使用“消息队列 + 最终一致性”,性能最高。
- 能不分就不分:如果业务逻辑允许,尽量通过数据冗余或合并,避免跨库关联。
👋 我是数据库小学妹,一个用设计师思维学数据库的转行人。你在项目中遇到过分布式事务问题吗?用了什么方案?欢迎讨论!
本文方案不限于特定中间件,RocketMQ、Kafka、Seata等均可实现。生产环境需根据业务容忍度选择合适方案。