分布式事务解决方案:最佳实践(一个刚裸辞的前端仔被迫搞后端的血泪史)
上周五晚上,我关掉了最后一台云服务器的监控告警,正式结束了我在某厂“宇宙级电商中台”项目组快两年的搬砖生涯。没错,就是那个双11凌晨三点全员蹲在会议室啃泡面、运维兄弟一边骂娘一边扩容、产品经理拿着Excel表说“这个需求很简单”的地方。
其实我本是个正经前端,热爱CSS动画到能写一篇《贝塞尔曲线与爱情》的程度。但谁让我摊上了一个“全栈即正义”的技术总监?去年Q3,团队要重构用户积分系统——涉及账户、订单、营销三个微服务,跨库操作多得像我前同事的发际线。领导一句“你前端交互强,懂数据一致性,来牵头吧”,我就被推进了分布式事务的深坑。
更魔幻的是,面试官最近老爱问:“你们怎么保证分布式事务一致性的?” 我心想,这不就是我过去半年每天和死锁、超时、幂等性斗智斗勇的日常吗?今天趁刚辞职、记忆还热乎,写点实战经验,顺便帮正在刷面试题挑战的兄弟们避雷。
问题背景:不是所有“成功”都真的成功
我们的积分系统逻辑看似简单:
- 用户下单成功 → 扣减商品库存(库存服务)
- 同步增加用户积分(账户服务)
- 记录营销活动参与(营销服务)
但在高并发下,比如去年双11秒杀,经常出现“库存扣了,积分没加”的鬼畜场景。用户投诉爆炸,客服小妹差点把我微信拉黑。
根本原因?三个服务各自用独立数据库(MySQL + PostgreSQL + TiDB),传统单体事务 BEGIN ... COMMIT 直接失效。CAP理论摆在那,我们选了 AP(可用性+分区容错),但业务又要求强一致性——典型的“既要又要还要”。
解决方案选型:从“理想很丰满”到“现实打脸”
一开始,我和几个后端兄弟热血沸腾地调研了主流方案:
| 方案 | 优点 | 缺点 | 我们的实际体验 |
|---|---|---|---|
| 2PC (XA) | 强一致性 | 性能差、阻塞严重、数据库耦合 | 双11压测直接拖垮MySQL主库,滚回 |
| TCC | 灵活、性能好 | 代码侵入强、需实现Confirm/Cancel | 写到一半发现营销服务没法“取消参与活动”,放弃 |
| 消息队列最终一致性 | 解耦、异步、扩展性好 | 需处理消息丢失、重复消费 | 最终选择! |
我们最终选了 基于可靠消息的最终一致性,配合本地消息表 + RocketMQ(别问,问就是公司中间件栈已定)。
实战:Python 里的“事务”其实是“状态机”
虽然我是前端出身,但为了这个项目硬着头皮写了好几万行 Python(Django + Celery)。核心思路是:把分布式事务拆成多个本地事务 + 消息补偿。
第一步:本地事务 + 消息预提交
在订单服务里,下单成功的同时,往本地 outbox 表插入一条待发送消息:
# order_service/views.py
from django.db import transaction
from .models import Order, OutboxMessage
def create_order(user_id, items):
with transaction.atomic():
# 1. 创建订单(本地事务)
order = Order.objects.create(user_id=user_id, status='paid', ...)
# 2. 插入本地消息表(同库,保证原子性)
OutboxMessage.objects.create(
topic='积分增加',
payload={'user_id': user_id, 'points': calculate_points(items)},
status='pending'
)
return order
💡 关键点:
OutboxMessage和Order在同一个数据库,transaction.atomic()保证要么都成功,要么都失败。
第二步:异步投递消息(防丢失)
用 Celery 定时扫描 outbox 表,投递到 RocketMQ:
# tasks.py
@app.task
def send_pending_messages():
pending_msgs = OutboxMessage.objects.filter(status='pending')[:100]
for msg in pending_msgs:
try:
rocketmq_producer.send(msg.topic, msg.payload)
msg.status = 'sent'
msg.save()
except Exception as e:
# 失败则重试(指数退避)
msg.retry_count += 1
if msg.retry_count > 3:
msg.status = 'failed' # 告警人工介入
msg.save()
第三步:消费者幂等处理
账户服务消费消息时,必须幂等!否则重复消费会导致积分翻倍(真发生过,用户狂喜,财务暴怒):
# account_service/consumer.py
def handle_add_points(message):
user_id = message['user_id']
points = message['points']
message_id = message['message_id'] # MQ提供唯一ID
# 先查是否已处理
if ProcessedMessage.objects.filter(message_id=message_id).exists():
return # 幂等,直接返回
with transaction.atomic():
# 增加积分
Account.objects.filter(user_id=user_id).update(points=F('points') + points)
# 记录已处理
ProcessedMessage.objects.create(message_id=message_id)
踩过的坑(血泪总结)
消息顺序问题
积分增加和积分使用可能乱序。解决方案:对同一用户的消息按user_id哈希到同一 MQ 分区。本地消息表膨胀
没人清理outbox表,三个月后单表 2 亿条。后来加了定时任务:成功发送 7 天后自动归档。测试环境 vs 生产环境
本地跑得好好的,上线后 MQ 延迟飙升。原因是生产环境网络策略限制了长连接。运维大哥翻白眼:“你们开发能不能考虑下基础设施?”监控缺失
曾经有 3 小时消息堆积没发现,直到用户投诉。现在 Grafana 面板盯死:outbox_pending_count,mq_lag,failed_message_rate。
性能与架构考量
- 数据库设计:
outbox表加了(status, created_at)联合索引,避免全表扫描。 - 接口设计:所有服务提供
idempotency_key参数,前端生成 UUID 透传,后端校验。 - 降级方案:MQ 故障时,自动切到 DB 轮询模式(性能差但保底)。
线上效果:双11期间处理 50w+/分钟事务,最终一致性延迟 < 2s,错误率 < 0.001%。老板终于没在复盘会上点我名。
写在裸辞之后
现在坐在咖啡馆敲这段文字,突然觉得:分布式事务哪有什么银弹?不过是用复杂度换可靠性,再用监控和自动化兜底。
如果你也在准备面试,记住:别只背 Seata 或 Saga 的原理,讲清楚你踩过的坑、监控指标、降级方案,才是高分答案。
至于我?暂时不想碰任何带“事务”二字的东西了。下份工作,我想找个能专心调 CSS 动画的团队——或者,干脆转行去教小学生编程?
(完)
P.S. 文中代码为简化版,生产环境记得加熔断、限流、日志追踪。别学我当初图快,结果半夜被 PagerDuty 叫醒修 Bug。

评论 0