分布式事务解决方案:最佳实践
嗨,大家好!我是刚从英国回来不久的海归硕士,目前在深圳某家腾讯系公司做后端开发。回国找工作的那阵子,简历上写着“分布式系统研究”,结果面试官问:“那你来聊聊 Seata 和 TCC 啊?”我当场懵了——学校里确实学过 2PC、3PC 这些理论,但真到了生产环境,哪有这么理想化?
于是,在入职后的第一个大促项目(去年双11)里,我被安排负责一个核心支付模块的重构。需求很“简单”:用户下单、扣库存、生成账单、发消息队列、记录日志……一套下来要跨5个微服务,数据库还不在一个实例上。产品经理说:“保证最终一致性就行。” 我心想:说得轻巧,你倒是来写啊!
这篇文章,就是我在踩了无数坑、熬了几个通宵、甚至差点被运维小哥拉黑之后,总结出的一套分布式事务实战经验。希望对正在求职或刚入行的朋友们有点帮助。顺便,如果你也在深圳、也搞 Python 后端、也在为分布式事务头秃——欢迎私聊,咱俩可以一起吐槽 PM。
一场由“超卖”引发的血案
事情得从去年9月说起。我们团队接到一个紧急需求:新上线的秒杀活动,要求支持高并发下单,且不能超卖。听起来老生常谈?但我们的架构是这样的:
- 用户服务(MySQL A)
- 商品服务(MySQL B)
- 订单服务(MySQL C)
- 账务服务(MySQL D)
- 消息中心(RabbitMQ)
用户点击“立即购买” → 商品服务校验库存 → 订单服务创建订单 → 账务服务冻结金额 → 消息中心推送通知。任何一个环节失败,前面的操作必须回滚。
一开始,我天真地用了“本地事务 + 补偿逻辑”:先扣库存,再建订单,如果后面失败,就手动调用“加回库存”的接口。结果上线第一天,测试同学就跑来说:“你们这库存怎么越卖越多?” 原来在高并发下,多个线程同时读到库存为10,都以为自己能扣成功,结果实际扣了15次——典型的竞态条件。
我当时真的想砸电脑。更糟的是,第二天早上站会,PM笑眯眯地问:“这个 Bug 今天能修好吗?明天就要压测了哦~”
分布式事务的几种姿势
痛定思痛,我开始系统性地调研分布式事务方案。市面上主流的有这么几种:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC / 3PC | 两阶段/三阶段提交 | 强一致性 | 性能差、阻塞严重 | 传统银行系统 |
| TCC(Try-Confirm-Cancel) | 业务层面实现补偿 | 灵活、高性能 | 侵入性强、开发成本高 | 高并发电商 |
| Saga | 事件驱动 + 补偿链 | 易扩展、适合长流程 | 最终一致性、补偿复杂 | 订单、物流等 |
| MQ 事务消息 | 利用消息队列的事务机制 | 解耦、异步 | 依赖 MQ 可靠性 | 日志、通知类操作 |
| Seata(AT 模式) | 基于 undo log 自动回滚 | 对业务透明、易集成 | 需要代理数据源、性能损耗 | 中小型微服务 |
我们团队评估后,决定混合使用 TCC + MQ 事务消息。为什么?
- 核心链路(扣库存、建订单、冻资金)必须强一致,选 TCC
- 非核心(发通知、打日志)用 MQ 事务消息,保证最终一致
📌 小贴士:别迷信“银弹”。没有哪个方案能解决所有问题,关键是根据业务容忍度做取舍。我们 PM 虽然嘴上说“最终一致就行”,但其实超卖一秒都不行 😅
实战:用 Python 实现 TCC
我们后端主要用 Python(FastAPI + SQLAlchemy),所以得找一个对 Python 友好的 TCC 框架。可惜,Seata 官方只支持 Java,Python 社区的轮子又不太成熟。最后,我们决定手撸一个轻量级 TCC 协调器。
第一步:定义 Try / Confirm / Cancel 接口
以“扣库存”为例:
# inventory_service.py
from typing import Dict
class InventoryService:
def try_decrease(self, sku_id: str, num: int) -> bool:
"""尝试预留库存"""
# 1. 检查可用库存
# 2. 插入一条“预留”记录(状态=pending)
# 3. 返回是否成功
pass
def confirm_decrease(self, sku_id: str, num: int) -> bool:
"""确认扣减库存"""
# 将预留记录状态改为 confirmed,并减少 real_stock
pass
def cancel_decrease(self, sku_id: str, num: int) -> bool:
"""取消预留"""
# 删除预留记录 or 标记为 cancelled
pass
注意:Try 阶段不能真正扣减库存,只能“冻结”或“预留”。这是 TCC 的精髓——资源预占。
第二步:事务协调器(Transaction Coordinator)
我们用 Redis 做事务日志存储,记录每个全局事务的状态:
# tcc_coordinator.py
import redis
import json
from datetime import datetime
class TCCCoordinator:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, decode_responses=True)
def begin_transaction(self, tx_id: str, services: List[Dict]) -> bool:
"""开启全局事务"""
tx_log = {
'tx_id': tx_id,
'status': 'TRYING',
'services': services,
'created_at': datetime.utcnow().isoformat()
}
self.redis.set(f"tcc:tx:{tx_id}", json.dumps(tx_log))
return True
def try_phase(self, tx_id: str) -> bool:
"""执行所有服务的 Try"""
# 调用各服务的 try 接口
# 如果全部成功,更新状态为 'TRY_SUCCESS'
# 否则,标记为 'TRY_FAILED' 并触发 Cancel
pass
def confirm_or_cancel(self, tx_id: str):
"""根据 Try 结果决定 Confirm 还是 Cancel"""
tx_log = json.loads(self.redis.get(f"tcc:tx:{tx_id}"))
if tx_log['status'] == 'TRY_SUCCESS':
# 并行调用所有 Confirm
self._parallel_call('confirm')
self.redis.set(f"tcc:tx:{tx_id}:status", "CONFIRMED")
else:
# 并行调用所有 Cancel
self._parallel_call('cancel')
self.redis.set(f"tcc:tx:{tx_id}:status", "CANCELLED")
💡 踩坑提醒:
- 网络超时:Try 成功但没收到响应,协调器以为失败,结果 Cancel 了,但服务端其实已经预留了资源 → 需要幂等 + 状态机
- 悬挂问题:Cancel 先于 Try 执行(比如网络延迟)→ Try 必须检查是否有 Cancel 记录
- 空回滚:Try 没执行,但 Cancel 被调用了 → Cancel 要能处理“无事可做”的情况
我们为此专门写了幂等中间件和状态校验装饰器,代码量翻了一倍,但稳定性提升巨大。
MQ 事务消息:兜底的“后悔药”
对于发短信、打日志这类操作,我们用了 RabbitMQ 的Publisher Confirms + 消息表模式。
流程如下:
- 本地事务:插入业务数据 + 插入“待发送消息”到
message_outbox表 - 事务提交后,后台任务扫描
message_outbox,发送消息 - 消息发送成功,删除记录;失败则重试(带退避策略)
# order_service.py
def create_order(user_id, items):
with db.transaction():
order = Order.create(...)
MessageOutbox.create(
msg_type="order_created",
payload=json.dumps({"order_id": order.id}),
status="pending"
)
# 此时事务已提交,消息一定会被投递
这种模式的好处是:
- 不依赖 MQ 的事务特性(RabbitMQ 本身不支持事务消息,不像 RocketMQ)
- 消息与业务数据在同一个 DB,保证原子性
- 即使 MQ 挂了,消息也不会丢
上周五晚上,RabbitMQ 集群真的崩了,但我们的订单没受影响——因为消息都躺在 DB 里,等 MQ 恢复后自动重发。运维小哥终于没来找我骂人了 😌
性能优化:别让分布式事务拖垮 QPS
TCC 虽然灵活,但三次网络调用(Try + Confirm/Cancel)对性能影响不小。我们做了几项优化:
- 批量处理:把多个小事务合并成一个大事务(比如一次扣多个 SKU 库存)
- 异步 Confirm:Try 成功后,Confirm 放到 MQ 异步执行(牺牲一点强一致,换性能)
- 缓存库存:用 Redis 缓存库存,Try 阶段先扣 Redis,定时同步到 DB(需防 Redis 宕机)
最终,在 4C8G 的服务器上,我们的下单接口 P99 延迟从 800ms 降到 220ms,QPS 从 300 提升到 1500+。大促当天,零超卖,零资损。Leader 在群里发了个红包,我抢到了 8.88 —— 虽然不多,但比被叫去复盘事故强多了。
给求职者的建议:别只背八股文
现在回想起来,如果当初面试前多看看 Seata 源码、多动手写写 TCC demo,可能就不会在入职初期那么狼狈。很多同学(包括我)以为“分布式事务”就是背几个名词,但真正的难点在于落地细节:
- 如何设计幂等?
- 如何监控事务状态?
- 如何快速定位悬挂事务?
- 如何做灰度发布?
我在准备跳槽时,专门用 Python 写了一个 mini-tcc 开源项目(虽然 star 只有个位数 😂),面试时直接甩链接,比干讲理论管用多了。
🔥 求职 tip:
如果你在简历写“熟悉分布式事务”,请确保你能回答:
- “TCC 的空回滚怎么解决?”
- “Seata 的 AT 模式底层原理是什么?”
- “如果 Confirm 阶段失败了怎么办?”
面试官最爱问这些“边缘 case”。
写在最后
分布式事务不是银弹,而是一种权衡的艺术。它解决的不仅是技术问题,更是信任问题——让用户相信你的系统不会多扣钱、不会超卖、不会丢数据。
作为刚回国的“技术新人”,我很庆幸能在深圳这样一个互联网氛围浓厚的城市工作。腾讯系公司虽然卷,但技术分享文化很浓:每周都有内部 Tech Talk,大佬们毫不吝啬地分享踩坑经验。上周还有位 senior engineer 分享了他们用 Saga + Event Sourcing 重构计费系统的经历,听得我直呼内行。
如果你也在求职路上,或者正在为分布式事务头秃——别慌。每一个线上事故,都是你简历上未来的故事。毕竟,没有 debug 过分布式事务的程序员,不足以谈人生 😎
(完)
P.S. 本文所有代码均为简化示例,生产环境请务必加上重试、监控、告警、熔断等机制。别问我怎么知道的……

评论 0