从一次支付失败谈起:我们的分布式事务实践之路
大家好,我是一名在后端开发领域摸爬滚打了近十年的程序员。今天想和大家分享一次我在实际项目中踩坑并成功走出来的分布式事务实践经验。
事情要回到两年前,我所在的团队负责一个面向零售行业的电商平台重构项目。随着业务规模扩大,原本单体应用逐渐吃不消了,我们决定将其拆分为多个微服务:订单服务、库存服务、支付服务、用户中心等。刚开始一切看起来很顺利,直到上线后的某一天,用户投诉“付款成功但订单未创建”,而且库存还被扣减了!
这下子问题就暴露出来了——数据一致性崩了。
遇到的问题:支付与下单之间的“中间态”


在传统单体架构中,我们通过数据库事务很容易保障下单、扣库存、支付这些操作的一致性。比如用一条 SQL 就能搞定:
BEGIN TRANSACTION;
UPDATE inventory SET stock = stock -1 WHERE product_id = 123;
INSERT INTO orders (user_id, product_id) VALUES (456, 123);
COMMIT;
可一旦拆分成服务化架构,每个服务都拥有自己的数据库,这时再用本地事务显然已经行不通。我们当时的架构长这样:
[用户下单] -> [订单服务] -> 下单 + 调用支付服务 -> 扣库存
问题就出在这个链路里:
- 支付服务返回成功,但订单服务写入失败 → 用户钱没了,订单没生成
- 或者订单写了,库存没有扣 → 出现超卖
- 还有可能是调用链中断,系统处于半状态
当时我们用的是一种简单粗暴的“事后补偿机制”:定时跑批对账,把异常数据捞出来人工处理。这种方式短期还行,但随着交易量上涨(峰值达每秒上万笔),对账效率越来越低,甚至出现了几个小时的数据延迟,客户体验极差。
我们意识到,必须找到一个更成熟的分布式事务解决方案。
解决方案设计:从 TCC 到消息队列的演变

起初我们尝试引入 TCC(Try-Confirm-Cancel)模型来实现分布式事务管理。TCC 模型的核心思想是把一个业务逻辑拆成三个阶段:
- Try:资源预留(冻结)
- Confirm:执行操作(扣款/扣库存)
- Cancel:回滚操作(解冻)
我们在订单服务中为下单流程加入 Try 阶段:
def try_order():
if payment_service.freeze(user_id, amount):
if inventory_service.reserve(product_id):
return True
return False
如果任意环节失败,直接触发 Cancel:
def cancel_order():
payment_service.unfreeze(user_id, amount)
inventory_service.release(product_id)
而在 Confirm 阶段才最终确认:
def confirm_order():
payment_service.deduct()
inventory_service.consume()
order.save()
但很快发现一个问题:这种同步模式大大降低了整个系统的吞吐能力。因为所有操作需要串行等待 Try 成功才能继续,同时还需要协调多服务的 Cancel 状态一致,维护成本很高。
后来我们采用了“异步+事件驱动”的方式做了优化,引入 Kafka 作为事务消息中间件。核心思想是:将业务动作通过事件发布出去,下游服务监听事件并自行处理事务。
大致流程如下:
- 用户下单,订单服务写入本地事务日志,并发送一个
OrderCreatedEvent; - 支付服务监听到此事件,启动本地事务进行支付;
- 库存服务监听同一事件,执行冻结库存;
- 支付完成后发
PaymentSucceedEvent,库存服务监听后正式扣减; - 订单服务监听支付结果更新订单状态。
这个过程中,我们重点做了三件事:
- 使用本地事务表记录每一个事件是否成功消费;
- 引入死信队列处理重试失败的消息;
- 为关键事件设计幂等性校验,防止重复消费。
代码结构大致如下(Python伪代码):
def create_order(data):
db.begin()
# 创建订单
order = Order(**data).save()
# 发送事件
event = {
"type": "ORDER_CREATED",
"order_id": order.id,
"product_id": data["product_id"],
"amount": data["total_amount"]
}
kafka_producer.send("order_event", value=json.dumps(event))
db.commit()
消费者侧:
def on_order_created(msg):
event = json.loads(msg.value)
# 幂等检查
if redis.exists(f"processed:{event['order_id']}"):
return
try:
# 执行业务逻辑
deduct_payment(event["amount"])
reserve_inventory(event["product_id"])
# 标记已处理
redis.setex(f"processed:{event['order_id']}", 86400, 1)
except Exception as e:
log.error(f"Failed to process event: {e}")
retry_event(event)
实战中的那些坑

这一路走下来,踩了不少坑,也积累了很多教训:
坑一:消息丢失导致状态不一致
初期没有使用带事务特性的 Kafka Producer,出现过因网络中断导致的消息丢失,结果就是有的服务收到了事件,有的没有收到。
解决办法:我们启用了 Kafka 的事务特性(Transaction API),并在订单创建时开启事务,确保写库与发消息在一个事务内提交或回滚:
producer.init_transactions()
try:
producer.begin_transaction()
# 写数据库
create_order_in_db(...)
# 发消息
producer.send(...)
producer.commit_transaction()
except Exception:
producer.abort_transaction()
坑二:消费者重复消费导致资金损失
某个凌晨,客服反馈有用户重复被扣款。排查后发现是因为消费者重启导致位点重置,Kafka 拉取到了历史消息重新消费。
解决方法:给每个业务事件增加唯一 ID,在消费者端建立防重表或使用 Redis 缓存最近 24 小时内的已处理事件 ID。
坑三:事件顺序不一致引发错误状态
在高峰期,多个事件可能被并发消费,比如支付成功的事件先于订单创建到达消费者,这时候库存服务会找不到对应订单而报错。
解决方法:为事件加上依赖关系标识,并在消费者端引入状态机控制事件处理顺序。
上线后的效果与收益

这套方案上线后,最明显的改善体现在以下几个方面:
- 整体交易成功率提升了 97%,异常处理时效从小时级降到秒级;
- 系统可用性和伸缩性增强,即使某个服务故障,也能通过消息重试机制缓解;
- 数据一致性得以保障,用户投诉率下降明显;
- 后期接入新业务模块变得更轻松,事件总线解耦了服务间的直接依赖。
我的几点建议
如果你也在做类似的事情,我分享几个经验:
- 不要迷信任何一种“万能方案”。TCC 适合短周期强一致性场景,但复杂度高;事务消息适合异步长周期场景,但需要引入额外组件。
- 幂等性是分布式系统的生命线。一定要在设计阶段就想清楚每个接口和事件的幂等逻辑。
- 监控和告警不能省。我们在部署方案的同时接入了 Prometheus + Grafana,实时监控各个服务的事务状态。
- 本地事务表是个好东西。哪怕你用的是第三方消息系统,也要有个本地持久化机制配合处理。
- 性能和一致性之间需要权衡。有时候“最终一致性”比“强一致性”更容易落地,特别是在大规模分布式系统中。
结语
回想这段经历,其实每一次技术选型的背后都是痛苦的思考和多次迭代。分布式事务这件事,从来就没有银弹。但只要你愿意沉下心去思考问题本质,结合业务特点灵活运用各种方案,就一定能找到最适合自己的那条路。
希望这篇实战经验对你有所启发。如有任何交流想法,欢迎留言讨论,共同进步!

评论 0