分布式事务解决方案:最佳实践(一个被Cursor惯坏的后端仔的血泪总结)
成都的夏天闷热得离谱,但好在我是早起型选手——每天8点准时坐到工位上,咖啡还没凉,代码就已经跑起来了。自从去年彻底投奔 Cursor 的怀抱,写代码这件事对我而言已经从“搬砖”进化成了“指挥AI打工”。说真的,现在让我手动敲完一个完整的 Service 层,手都会抖。
不过,AI 再聪明,也救不了架构设计上的坑。上周五晚上 10 点,我还在公司死磕一个线上分布式事务问题,产品经理在群里疯狂@:“双11预热活动明天上线,这个订单状态不一致的问题必须今晚搞定!” 我盯着屏幕里那条诡异的 OrderStatus = PAID 但 InventoryService 显示库存未扣减的日志,内心万马奔腾——这哪是写代码,这是在玩火。
今天这篇文章,就是我用血泪换来的分布式事务实战笔记。不讲理论堆砌,只聊怎么在真实业务中把性能、稳定性、可维护性都拉满。顺便安利几本我看过的神书,以及为什么我觉得“代码人生”不该只是 CRUD。
背景:我们到底在怕什么?
先说清楚场景。我们团队做的是一个电商聚合平台,后端服务拆得比较细:
- OrderService:处理下单、支付回调
- InventoryService:管理商品库存
- CouponService:核销优惠券
- LogisticsService:生成物流单
典型的“一笔订单,四个服务”的微服务架构。用户支付成功后,OrderService 收到支付宝回调,然后依次调用其他三个服务。听起来很顺?现实是:网络会超时、服务会宕机、数据库会主从延迟。
去年双11,我们就因为 InventoryService 短暂不可用,导致 Order 状态变成 PAID,但库存没扣。结果就是——超卖。客服电话被打爆,老板脸色比成都的雾霾还黑。
这时候你可能会说:“用两阶段提交(2PC)啊!” 拜托,2PC 在高并发下就是性能杀手,锁表时间长到能让你泡三杯茶。而且我们的 MySQL 集群扛不住这种全局锁。
所以问题核心就两个:
- 如何保证多个服务的数据最终一致?
- 如何在不牺牲吞吐量的前提下做到这一点?
方案选型:别被论文忽悠瘸了
我翻过《Designing Data-Intensive Applications》(这本书真香,建议人手一本),也啃过《微服务架构设计模式》,但落地时发现:学术方案和生产环境之间,隔着一条运维的鸿沟。
我们评估了三种主流方案:
| 方案 | 一致性级别 | 性能影响 | 运维复杂度 | 是否适合我们 |
|---|---|---|---|---|
| 2PC (XA) | 强一致 | ⚠️ 高(锁资源久) | 中 | ❌ |
| TCC | 最终一致 | ✅ 低(无长事务) | ⚠️ 高(需补偿逻辑) | ⚠️ |
| 基于消息队列的最终一致 | 最终一致 | ✅ 低 | ✅ 低 | ✅ |
TCC 虽然性能好,但每个业务都要写 Try/Confirm/Cancel,代码量爆炸。我们团队就 5 个后端,还有一个在摸鱼学爬虫(别问,问就是“技术调研”),根本扛不住。
于是我们拍板:用消息队列 + 本地事务表,实现最终一致性。核心思想就一句:先在本地事务里把“要发的消息”和“业务数据”一起写进 DB,再异步投递。
实战:代码怎么写才不翻车?
第一步:建一张“事务消息表”
CREATE TABLE transactional_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
business_key VARCHAR(64) NOT NULL, -- 订单ID
message_topic VARCHAR(128) NOT NULL,
message_body JSON NOT NULL,
status TINYINT DEFAULT 0, -- 0:待发送, 1:已发送, 2:发送失败
retry_count INT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status_retry (status, retry_count),
UNIQUE KEY uk_bizkey_topic (business_key, message_topic)
);
关键点:
- 和业务表在一个 DB,保证本地事务原子性
status + retry_count支持重试机制uk_bizkey_topic防止重复消息(幂等性基石)
第二步:下单逻辑(伪代码)
@Transactional
public void createOrder(OrderRequest req) {
// 1. 扣库存(调用 InventoryService,同步)
inventoryClient.decrease(req.getProductId(), req.getCount());
// 2. 创建订单(本地 DB)
Order order = new Order(...);
orderRepo.save(order);
// 3. 插入事务消息(和订单同一个事务!)
TransactionalMessage msg = new TransactionalMessage();
msg.setBusinessKey(order.getId());
msg.setMessageTopic("order-created");
msg.setMessageBody(JsonUtil.toJson(order));
msgRepo.save(msg); // 和 order 在同一个 TX
}
看到没?Inventory 是同步调用,但 Coupon 和 Logistics 用消息异步解耦。为什么?因为库存不能超卖,必须强校验;而优惠券核销失败可以人工补,物流单晚生成几分钟用户也无感。
📌 吐槽:产品经理曾要求“所有操作必须实时完成”,我反问:“如果物流系统挂了,用户就不能下单了?” 他沉默了。
第三步:消息投递服务(独立进程)
我们写了个轻量级的 MessageRelay 服务,每 500ms 扫一次 transactional_message 表,捞出 status=0 的记录,投到 Kafka。
@Scheduled(fixedDelay = 500)
public void relayMessages() {
List<TransactionalMessage> pending = msgRepo.findPending(100);
for (TransactionalMessage msg : pending) {
try {
kafkaTemplate.send(msg.getTopic(), msg.getBody());
msg.setStatus(1);
msgRepo.updateStatus(msg); // 标记成功
} catch (Exception e) {
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() >= MAX_RETRY) {
msg.setStatus(2); // 失败,告警+人工介入
}
msgRepo.updateStatus(msg);
}
}
}
性能优化点:
- 批量查询 + 批量更新(减少 DB I/O)
- 消息体用 JSON 而不是序列化对象(兼容多语言消费者)
- Kafka 分区按
business_keyhash,保证同一订单的消息顺序
坑与填坑:那些让我想砸键盘的瞬间
坑1:消息重复消费
Kafka 不保证 exactly-once(虽然有事务 API,但性能差)。消费者必须幂等!
我们的做法:
- 每个消费者建一张
message_consume_record表,记录已处理的message_id - 消费前先查表,存在就跳过
@Transactional
public void onOrderCreated(Message msg) {
if (consumeRecordRepo.existsByMessageId(msg.getId())) {
return; // 幂等
}
// 执行业务逻辑...
couponService.useCoupon(msg.getOrder().getCouponId());
consumeRecordRepo.save(new ConsumeRecord(msg.getId()));
}
坑2:本地事务和消息发送的“伪原子性”
早期我们犯了个低级错误:先 commit 业务事务,再发消息。结果 DB 提交成功,服务 crash,消息丢了。
正确姿势:消息必须和业务数据在同一个本地事务里持久化。投递可以异步,但“要不要投”这个决策必须原子化。
坑3:死信队列没人看
重试 5 次后消息进死信队列,但没人监控。直到某天运营发现一批订单没生成物流单。
现在我们:
- 死信消息自动触发企业微信告警
- 每天凌晨跑脚本,把超过 24h 的失败消息汇总成报表
- 运维兄弟每周五下午“清垃圾”,成了固定仪式(他说这叫“数字禅修”)
性能数据:到底快不快?
我们在压测环境跑了对比(4C8G * 3 节点):
| 场景 | TPS | P99 延迟 | 错误率 |
|---|---|---|---|
| 同步调用所有服务 | 120 | 850ms | 0.5%(超时) |
| 2PC (Seata AT) | 90 | 1200ms | 0.1% |
| 消息最终一致(本文方案) | 380 | 210ms | 0.02% |
结论:吞吐量提升 3 倍+,延迟砍掉 75%。而且错误基本都是网络抖动,重试即可恢复。
写在最后:代码人生,不止于跑通
说实话,搞分布式事务挺折磨人的。但每次看到系统稳稳扛住流量高峰,心里又有点小得意。这大概就是“代码人生”的魅力——你写的每一行,都在真实世界产生涟漪。
最近我在用 Cursor 辅助重构这套消息中继服务。它不仅能自动生成幂等检查的模板代码,还能根据我的注释自动写单元测试。以前要花半天的活,现在喝杯咖啡就搞定。AI 不是取代程序员,而是把我们从重复劳动里解放出来,去思考更酷的架构问题。
如果你也在折腾分布式系统,推荐两本书:
- 《Designing Data-Intensive Applications》:数据库和分布式基础圣经
- 《凤凰项目》:用小说讲 DevOps,读起来像追剧
对了,那个学爬虫的同事,上周用 Python 写了个脚本,自动抓取竞品的库存变动,喂给我们的预测模型。结果准确率提升了 15%。看来“不务正业”有时候也能创造价值?
总之,分布式事务没有银弹,但只要抓住“本地事务保原子,消息队列保最终一致,幂等性兜底”这三板斧,再配合合理的监控和告警,就能在性能和可靠性之间找到平衡点。
好了,成都的太阳快落山了,我也该收拾东西去吃火锅了。毕竟,再复杂的系统,也抵不过一顿毛肚鸭血的治愈力。
P.S. 如果你也在用 Cursor,欢迎交流 prompt 技巧!我已经整理了一套“后端架构师专用提示词库”,评论区留言“求分享”我就发 GitHub 链接~

评论 0