分布式事务解决方案:最佳实践——一个异地奶爸深夜敲代码的血泪总结
上周五晚上11点47分,终于把两个娃哄睡了。
老大三岁半,老二刚满一岁。老婆在另一个城市工作,每周五晚上的高铁回来,周日晚上又得走。这周她临时加班,没赶上车,我一个人扛下了“双娃夜战”。洗澡、讲故事、喂奶、换尿布……一套流程下来,感觉比跑完半马还累。
但我知道,真正的“工作时间”才刚刚开始。
打开电脑,泡了杯速溶咖啡(别笑,家里奶粉都快堆成山了,哪还有闲钱买手冲),我继续啃那个卡了三天的分布式事务问题。项目上线在即,订单服务和库存服务跨微服务调用,事务一致性成了拦路虎。老板说:“下周必须上线”,而我连Saga模式和TCC的区别都还没彻底搞明白。
那一刻,我坐在儿童房的小板凳上(主卧被娃占领了),盯着屏幕上密密麻麻的代码,心里一阵发虚:月薪从15k涨到22k是好事,但技术债不会因为工资涨就自动消失。
为什么分布式事务让我彻夜难眠?
先说背景。我们系统用的是Spring Cloud + Dubbo + MySQL,典型的Java微服务架构。用户下单时,要同时扣减库存、生成订单、记录积分。三个服务,三个数据库,传统单体时代的@Transactional直接失效。
去年十月第一次遇到这个问题时,我天真地以为加个try-catch+手动回滚就行。结果上线第二天,库存超卖,客服电话被打爆。老板脸色铁青,我躲在工位上改bug到凌晨三点——那天正好是老婆产检的日子,我在视频里看到她一个人站在医院走廊,手里拿着B超单。
技术可以重来,但家人的信任一旦崩了,重建很难。
从那以后,我下定决心:必须系统性掌握分布式事务。不是为了KPI,而是为了夜里能安心睡觉,而不是被报警短信惊醒。
工具选型:踩坑后的清醒认知
市面上主流方案无非几种:2PC、TCC、Saga、本地消息表、Seata、RocketMQ事务消息……听起来高大上,但落地时全是细节魔鬼。
1. 别迷信“全自动”——Seata的甜蜜陷阱
我一开始狂推Seata,AT模式看起来太香了:无侵入、自动回滚、一行注解搞定。团队里几个新人也跟着喊“牛逼”。
但现实狠狠打了脸。
- 性能瓶颈:全局锁导致高并发下吞度量暴跌,压测时TPS从800掉到120。
- 脏读风险:AT模式依赖undo_log,但在极端网络分区下,可能读到中间状态。
- 运维复杂:TC(Transaction Coordinator)要单独部署,还要考虑高可用。有次TC挂了,整个下单链路瘫痪两小时。
最后我们只在非核心场景用了Seata。工具再强,也得看业务容忍度。
2. TCC:看似优雅,实则“自虐”
TCC(Try-Confirm-Cancel)要求每个服务实现三个接口。理论上很完美,但开发成本爆炸。
举个例子:扣库存的try阶段要预占库存,confirm真正扣减,cancel释放。听起来合理,但:
- 业务逻辑被强行拆成三份,代码可读性暴跌
- 网络超时、重复调用、幂等性……每个环节都要额外处理
- 测试用例翻倍,QA同事差点跟我翻脸
我们试了一个月,最终放弃。除非你是金融级系统,否则TCC的ROI(投入产出比)太低。
3. 本地消息表 + 异步补偿:打工人の务实之选
现在我们主力方案是:本地消息表 + RocketMQ事务消息 + 定时对账补偿。
具体做法:
- 下单时,在同一个DB事务中插入订单 + 插入“待发送消息”记录
- 后台线程扫描消息表,发送MQ
- 消费端处理成功后,更新消息状态
- 定时任务每天跑一次对账,修复不一致数据
优点很明显:
- 强依赖少:只用MySQL + MQ,都是现有技术栈
- 最终一致性:业务能接受几秒延迟(用户也不在乎)
- 可追溯:消息表就是天然日志,排查问题方便
缺点?当然有——要写补偿逻辑,要处理消息重复,要防死信队列堆积。但比起半夜被叫起来救火,这点工作量算什么?
成年人的世界,没有完美方案,只有权衡后的最优解。
Java 实践:代码里的血与泪
贴一段我们现在的核心伪代码(敏感信息已脱敏):
@Transactional
public void createOrder(OrderDTO dto) {
// 1. 本地事务:创建订单 + 插入消息
Order order = orderMapper.insert(dto);
Message message = new Message();
message.setBizId(order.getId());
message.setTopic("ORDER_CREATED");
message.setStatus(MessageStatus.PENDING);
messageMapper.insert(message); // 同一个DB事务!
// 2. 异步发送MQ(由独立线程池处理)
mqSender.asyncSend(message);
}
消费端:
@RocketMQMessageListener(topic = "ORDER_CREATED", consumerGroup = "inventory-group")
public class InventoryConsumer implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt msg) {
try {
// 幂等处理:先查是否已处理
if (messageService.isProcessed(msg.getMsgId())) {
return;
}
// 扣库存
inventoryService.decreaseStock(...);
// 标记消息已消费
messageService.markAsProcessed(msg.getMsgId());
} catch (Exception e) {
// 记录失败,等待补偿任务重试
messageService.markAsFailed(msg.getMsgId(), e.getMessage());
throw new RuntimeException("消费失败,触发重试", e);
}
}
}
关键点:
- 本地事务保证“订单+消息”原子性
- 消费端必须幂等(MQ至少一次投递)
- 失败必须可追踪,不能静默丢弃
这套方案上线三个月,0数据不一致。虽然不够“高大上”,但稳如老狗。
给 fellow 程序员的真心话
写到这里,已经是凌晨2点15分。老二突然哭醒,我赶紧去冲奶粉。看着他小口小口喝着,心里却在想:技术人最大的误区,就是追求“银弹”。
分布式事务没有银弹。Seata不是,TCC不是,甚至区块链也不是。真正的最佳实践,是理解业务边界,选择能驾驭的方案,然后用工程手段兜底。
如果你也在异地带娃、深夜coding,我想说:
- 别怕承认“我不会”——我曾因不懂Saga被同事质疑能力,后来花两周啃完《Designing Data-Intensive Applications》
- 工具只是杠杆,核心是思维——理解CAP、BASE、幂等性,比背框架源码更重要
- 保持输出倒逼输入——这篇文章除了帮别人,更是帮我理清思路
写在最后:为了更好的明天
老婆昨天视频问我:“你最近是不是又瘦了?”
我说:“没事,就是晚上多学点东西。”
她沉默了几秒,说:“别太拼,我和孩子需要你活着,不是透支。”
那一刻,我鼻子一酸。
技术人的价值,不该用加班时长衡量。能准时下班陪孩子,能周末带老婆逛超市,能在系统稳定时安心睡觉——这才是真正的“高可用人生”。
分布式事务的终点,从来不是代码完美,而是生活平衡。
共勉。

评论 0