分布式事务解决方案:一个边带娃边写代码的全职妈妈的踩坑实录
杭州某小区,凌晨2点。
娃刚睡,我偷偷摸出键盘,打开IDEA——别问,问就是“趁热打铁”,再不写完这篇复盘,明天晨会又得被P0级bug支配。
大家好,我是小禾,前阿里系后端开发,现自由职业者(俗称“在家带娃顺便接点活”)。坐标杭州,离西溪园区就两站地铁,偶尔还能蹭蹭网易食堂(嘘,别告诉门卫)。最近在研究Rust,但今天不是讲Rust,而是讲一个让我上周五加班到娃睡觉都没哄成的“老熟人”——分布式事务。
事情是这样的:我接了个外包项目,客户是个做SaaS电商系统的创业公司,用的是 Spring Boot + MySQL + Redis 架构。系统里有个核心流程:用户下单 → 扣减库存 → 创建支付单 → 发券。四个服务跨三个数据库,典型的“一写多读+状态同步”场景。一开始他们用最朴素的“try-catch rollback”大法,结果双11压测时直接炸了:订单创建成功了,库存没扣,券发了两遍……测试小姐姐直接甩锅给我:“你这系统比我家猫还不可靠!”
我差点当场表演一个原地辞职。但转念一想,这不正是个练手分布式事务的好机会?而且——能拿钱解决问题,总比半夜被娃哭醒强吧?
为什么“本地事务”在微服务时代彻底不够用了?
先说句大实话:如果你还在用 @Transactional 解决跨服务一致性问题,那你可能和我一样,曾经天真地以为世界是单体的。
在单体应用里,一个方法加个注解,数据库 ACID 自动搞定。但一旦拆成微服务,每个服务都有自己的 DB,你的“下单”服务调“库存”服务,本质上就是两个独立事务。网络可能抖、服务可能挂、DB 可能主从延迟……这时候,要么全部成功,要么全部失败 的要求,就变成了分布式系统里的“薛定谔的猫”——你永远不知道它到底死了没死。
更惨的是,我们这个项目还混用了 Python 写的数据分析模块(客户坚持要用 Pandas 处理营销数据),所以整个链路其实是:
Spring Boot (Java) → Spring Boot (Java) → Python Flask → Spring Boot (Java)
是的,你没看错。一个事务横跨 Java 和 Python。我当时看到架构图的时候,真的想砸电脑。
我试过的三种方案:从“理想很丰满”到“现实很骨感”
方案一:两阶段提交(2PC)——理论上完美,实际上劝退
我第一反应是上 XA 事务,毕竟教科书都说这是“标准答案”。Spring Boot 通过 Atomikos 或 Bitronix 支持 JTA,配置也不难:
# application.yml
spring:
jta:
enabled: true
atomikos:
datasource:
xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
但问题来了:
- 性能极差:准备阶段要锁资源,高并发下直接卡成 PPT。
- 不支持异构语言:Python 那边怎么参与 XA?人家连 JNDI 是啥都不知道。
- 运维噩梦:一旦协调者挂了,参与者可能长时间阻塞,DB 连接池直接耗尽。
上线前压测,QPS 从 1500 直接掉到 80。产品经理看了监控图,幽幽地说:“我们是不是该考虑换人了?”
结论:2PC 适合银行核心系统这种对一致性要求极高、吞吐量低的场景。对我们这种互联网快节奏项目?拜拜了您嘞。
方案二:消息队列 + 最终一致性 —— 我的“真香”选择
被 2PC 毒打之后,我转向了业界更常用的 基于消息队列的最终一致性方案。核心思想就一句:用“可靠消息”代替“同步调用”。
具体到我们项目,流程改成这样:
- 下单服务创建订单(状态 = “待支付”),同时发一条 半消息 到 RocketMQ(阿里开源,杭州人用着亲切 😎)
- 库存服务消费消息,扣减库存;成功则 ACK,失败则重试
- 支付服务监听库存成功事件,创建支付单
- 营销服务监听支付成功事件,发券
关键点在于:消息必须可靠投递。这里我用了 RocketMQ 的 事务消息 机制,配合本地事务表。
关键代码:Spring Boot + RocketMQ 事务消息
// 1. 下单时发送半消息
@Transactional
public void createOrder(Order order) {
// 先写本地事务表(记录消息状态)
messageDao.insertPendingMessage(order.getId(), "ORDER_CREATED");
// 发送半消息
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
"ORDER_TOPIC",
MessageBuilder.withPayload(order).build(),
null
);
if (sendResult.getLocalTransactionState() != LocalTransactionState.COMMIT_MESSAGE) {
throw new RuntimeException("消息发送失败");
}
}
// 2. RocketMQ 回查本地事务状态
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 实际不会走这里,因为我们在 createOrder 里已经 commit 了
return RocketMQLocalTransactionState.UNKNOWN;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = extractOrderId(msg);
// 查询本地事务表:如果订单存在且状态正常,则提交消息
if (orderService.existsValidOrder(orderId)) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
Python 服务怎么接?
好消息是,消息队列天然解耦语言!Python 服务只需要订阅对应 Topic:
# consumer.py (Python Flask + rocketmq-client-py)
from rocketmq.client import PushConsumer
def handle_inventory_message(msg):
try:
order = json.loads(msg.body)
# 扣库存逻辑
inventory_service.deduct(order['sku_id'], order['quantity'])
return ConsumeStatus.CONSUME_SUCCESS
except Exception as e:
logger.error(f"扣库存失败: {e}")
return ConsumeStatus.RECONSUME_LATER # 稍后重试
consumer = PushConsumer('inventory_group')
consumer.subscribe('ORDER_TOPIC', handle_inventory_message)
consumer.start()
💡 安全提示:记得给消息加签名或加密!我们曾因测试环境 MQ 未鉴权,被隔壁团队误消费了一堆测试订单,差点发了 1000 张 99 折券……
方案三:Saga 模式 —— 复杂业务的“后悔药”
有些场景光靠“最终一致”不够,比如:需要支持业务回滚。比如用户下单后 10 分钟内取消,得把库存加回去、券收回。
这时候我就祭出了 Saga 模式。简单说,就是为每个正向操作配一个补偿操作(Compensating Transaction),形成一个“事务链”。
我们用 Eventuate Tram Saga(Spring Boot 生态友好)来实现:
// 定义 Saga
public class CreateOrderSagaData {
private String orderId;
private String userId;
// ...
}
// 正向步骤
public class ReserveInventoryStep implements SagaStep<CreateOrderSagaData> {
public void execute(CreateOrderSagaData data) {
inventoryClient.reserve(data.getSkuId(), data.getQuantity());
}
// 补偿操作:释放预留库存
public void compensate(CreateOrderSagaData data) {
inventoryClient.release(data.getSkuId(), data.getQuantity());
}
}
// 启动 Saga
sagaManager.create(CreateOrderSagaData.class)
.withData(new CreateOrderSagaData(orderId, userId))
.addStep(new ReserveInventoryStep())
.addStep(new CreatePaymentStep())
.addStep(new IssueCouponStep())
.execute();
⚠️ 血泪教训:Saga 的补偿操作必须是 幂等 的!我们第一次上线时,补偿接口没做幂等,网络超时重试导致库存多加了两次。运维大哥凌晨三点打电话:“你们是不是在给黑产送福利?”
安全与运维:那些文档不会告诉你的细节
1. 幂等性:不是“最好有”,是“必须有”
无论是消息消费还是补偿接口,所有写操作必须幂等。我的做法是:
- 消息带唯一 ID(如
orderId_eventType) - 本地记录已处理消息 ID(Redis Set + TTL)
- 数据库写操作用
INSERT ... ON DUPLICATE KEY UPDATE或UPDATE ... WHERE version = ?
-- 示例:幂等发券
INSERT INTO user_coupons (user_id, coupon_id, order_id, status)
VALUES (123, 'COUPON_2024', 'ORDER_789', 'ACTIVE')
ON DUPLICATE KEY UPDATE status = 'ACTIVE';
2. 监控告警:别等用户投诉才救火
我在 Grafana 上搭了三个关键看板:
- 消息积压量(RocketMQ 的
diffTotal) - 事务失败率(本地事务表中状态为“失败”的比例)
- 补偿操作触发次数(突然飙升说明正向流程有问题)
配上企业微信机器人,一有异常立马 @ 我。虽然经常半夜被吵醒,但总比第二天被客户骂强。
3. 数据核对脚本:每天凌晨跑一遍
再完善的方案也可能漏网之鱼。所以我写了 Python 核对脚本(感谢 Pandas),每天凌晨对比:
- 订单表 vs 库存流水
- 支付单 vs 券发放记录
发现不一致就自动生成工单,人工介入。这招救了我们好几次——有一次因为时区问题,券的有效期少算了一天,全靠核对脚本提前发现。
# reconciliation.py
import pandas as pd
orders = pd.read_sql("SELECT * FROM orders WHERE created_at > NOW() - INTERVAL 1 DAY", conn)
coupons = pd.read_sql("SELECT * FROM coupons WHERE order_id IN ({})".format(','.join(orders['id'])), conn)
# 找出有订单但没发券的
missing = orders[~orders['id'].isin(coupons['order_id'])]
if not missing.empty:
alert_to_feishu(f"发现 {len(missing)} 笔订单未发券!")
方案对比:一张表说清楚
| 方案 | 一致性级别 | 性能 | 异构语言支持 | 回滚能力 | 适用场景 |
|---|---|---|---|---|---|
| 2PC (XA) | 强一致 | 差 | ❌ | 自动 | 金融核心系统 |
| 消息队列 + 最终一致 | 最终一致 | 优 | ✅ | 手动 | 电商、社交等高并发场景 |
| Saga | 最终一致 + 补偿 | 中 | ✅ | 显式 | 需要业务回滚的长流程 |
对我们这种 混合 Java/Python、高并发、需部分回滚 的场景,消息队列 + Saga 补充 是最优解。
写在最后:当妈后,我对“鲁棒性”有了新理解
以前在阿里,我觉得“系统不能挂”是底线。现在带娃才发现,真正的鲁棒性,是在各种意外中依然能恢复——就像娃把牛奶打翻在键盘上,你一边擦一边还能提交代码。
分布式事务也一样。没有银弹,只有权衡。与其追求理论上的完美,不如设计一套 可监控、可追溯、可人工干预 的机制。毕竟,线上事故不可怕,可怕的是你不知道怎么救。
现在,我们的系统稳了。双11当天零资损,客户请我喝了杯喜茶(虽然是外卖)。而我最大的成就感?终于能在娃睡觉前写完代码,而不是在凌晨两点偷偷摸摸敲键盘。
哦对了,如果你也在杭州,想找人一起讨论 Rust 或分布式系统,欢迎约咖啡(娃托管时间有限,速来)!
附:避坑清单
- 别信“一次成功”的幻想,重试机制必须有
- 消息不要放敏感数据,加密或脱敏
- 补偿操作要测试“多次执行”场景
- 给所有异步任务加超时,别让线程池被占满
- 日志里打上 traceId,排查问题能快十倍
技术路上,我们都是在屎山里种花的人。共勉。

评论 0