分布式事务解决方案:一个从测试转开发的远程打工人血泪实战总结
去年十月,凌晨两点,我蹲在老家县城出租屋的阳台上,一边啃着冷掉的烤串,一边盯着屏幕上不断重试失败的订单。手机震动,产品经理小王发来消息:“哥,线上支付又出问题了,用户付了钱没到账,客服快被投诉炸了。”
我点开日志,一看就知道——又是分布式事务惹的祸。
当时真的想把键盘砸了。不是因为技术难(虽然确实不简单),而是因为我刚从测试岗转开发不到半年,月薪才从15k涨到22k,还没站稳脚跟,就撞上这种“史诗级”线上事故。更糟的是,我老婆还在隔壁房间睡觉,明天她还要早起送孩子上幼儿园——没错,我在老家远程办公,省下了北京3500块的房租,却换来了随时可能背锅的焦虑。
事情是怎么崩的?
我们做的是一款SaaS电商产品,核心流程是:用户下单 → 扣库存 → 创建支付单 → 调用第三方支付 → 支付成功后更新订单状态 + 发券 + 记账。
听起来很常规对吧?但问题在于,这些操作分布在四个微服务里:订单服务、库存服务、支付服务、营销服务。每个服务都有自己的数据库,彼此通过 HTTP 或 MQ 通信。
最初,团队为了“快速上线”,用了最朴素的方案:先调A,成功再调B,失败就回滚A。结果呢?支付成功了,但发券服务挂了,用户付了钱却没拿到优惠券;或者库存扣了,支付超时,订单卡在“待支付”,库存却锁死了。
上线第一周,客服工单暴涨300%。老板在晨会上黑着脸说:“这产品做得像个筛子。”
我被迫扛起了“填坑”的担子
作为团队里最“新鲜”的开发(其实就是背锅位),组长老李直接甩给我一句话:“你之前做测试,最懂数据一致性,这事你牵头搞。”
我内心OS:测试懂一致性 ≠ 开发会写分布式事务啊!
但没办法,远程办公的好处就是没人盯着你摸鱼,坏处就是出了事第一个被@。那天晚上,我翻遍了公司代码库、GitHub、Stack Overflow,甚至把《Java并发编程实战》翻出来找灵感——结果发现,书里根本没讲分布式事务怎么落地。
于是,我开始疯狂补课。花了一周时间,把主流方案过了一遍:
- 2PC(两阶段提交):理论上完美,但性能差、阻塞严重,而且Java生态里成熟的实现(比如Atomikos)和我们Spring Boot + MyBatis的架构集成起来巨麻烦。
- TCC(Try-Confirm-Cancel):灵活,但每个业务都要写三套逻辑,开发成本爆炸。我们一个小团队,哪有精力给每个接口都写Cancel?
- 本地消息表:靠谱,但需要额外建表、轮询、处理幂等,运维复杂度高。
- RocketMQ事务消息:看起来最香,但公司用的Kafka,临时切中间件?做梦。
最后,结合产品现状(中小规模、业务迭代快、团队人少),我拍板:用“可靠事件模式 + 幂等 + 补偿任务”组合拳。
实战:我是怎么在Java里落地的?
第一步:拆解核心链路,识别“关键事务边界”
我把整个支付流程画成一张图,标出哪些操作必须“全成功或全失败”。最终锁定两个核心事务组:
- 创建订单 + 扣库存(必须原子)
- 支付成功 + 发券 + 记账(必须原子)
注意:不要试图让所有操作在一个事务里完成。这是新手最容易踩的坑。分布式系统里,事务边界越小,越可控。
第二步:用“本地事务表”保证第一步的一致性
在订单服务里,我新建了一张 order_event 表,结构很简单:
CREATE TABLE order_event (
id BIGINT PRIMARY KEY,
order_id VARCHAR(64),
event_type VARCHAR(32), -- e.g., "DEDUCT_STOCK"
status TINYINT, -- 0: pending, 1: sent
payload JSON,
create_time DATETIME
);
当用户下单时,我这么做(伪代码):
@Transactional
public void createOrder(OrderRequest req) {
// 1. 插入订单
orderMapper.insert(order);
// 2. 插入库存扣减事件(状态为pending)
OrderEvent event = new OrderEvent();
event.setOrderId(order.getId());
event.setEventType("DEDUCT_STOCK");
event.setPayload(...);
event.setStatus(0);
eventMapper.insert(event);
}
然后,启动一个后台线程(或定时任务),扫描 status=0 的事件,调用库存服务。只有库存服务返回成功,才把事件状态改为1。
如果库存服务挂了?没关系,下次轮询继续重试。因为事件是持久化的,不会丢。
第三步:支付回调用“幂等 + 补偿”兜底
支付成功后,第三方会回调我们的接口。这里的关键是:回调可能重复、可能乱序、可能延迟。
所以,我在回调入口加了幂等锁:
public void handlePaymentCallback(PaymentCallbackDTO dto) {
String lockKey = "pay_callback:" + dto.getTradeNo();
if (redis.setNx(lockKey, "1", 300)) { // 5分钟过期
try {
// 检查是否已处理
if (orderService.isOrderPaid(dto.getOrderId())) {
return; // 已处理,直接返回
}
// 执行发券、记账等操作
couponService.issueCoupon(dto.getUserId(), ...);
accountingService.record(dto.getAmount(), ...);
// 更新订单状态
orderService.updateStatus(dto.getOrderId(), "PAID");
} finally {
redis.del(lockKey);
}
}
}
但如果发券服务在回调过程中挂了怎么办?这时候,我加了一个补偿任务:每天凌晨跑一个Job,找出“已支付但未发券”的订单,重新触发发券逻辑。
开发心得:别信“银弹”,信“合适”
折腾了两周,线上问题基本止住。客服工单降回正常水平,老板在周会上难得夸了句:“这次搞得很稳。”
但我知道,这不是技术多牛,而是选对了方案。
很多教程一上来就吹Seata、吹Saga,但现实是:小团队用不起重型框架,也扛不住复杂的运维成本。我们用最土的办法——本地表 + 轮询 + 幂等,解决了80%的问题。
另外,Java程序员容易陷入“框架依赖症”。总想着找个starter一键集成。但分布式事务的本质是业务设计 + 异常处理 + 监控告警,不是换个中间件就能搞定的。
我现在的原则是:
- 能同步就同步(比如订单+库存在同一DB)
- 必须异步的,就用事件驱动
- 所有下游操作必须幂等
- 关键链路加监控(比如事件积压告警)
远程办公的“副作用”:逼我学会独立思考
说实话,如果我在北京办公室,遇到这种问题,大概率会等架构师出方案,或者拉个会吵半天。但在老家,没人可问,只能自己啃。
反而逼出了成长。
上周五晚上,老婆看我对着屏幕傻笑,问我:“又涨工资了?”
我说:“没,但今天优化了一个补偿任务,把重试次数从10次降到3次,省了服务器资源。”
她翻了个白眼:“你是不是没救了?”
但我知道,这种“解决问题的爽感”,比涨薪还上头。
给同行的建议:从你的产品出发
如果你也在做类似系统,别急着抄作业。先问自己几个问题:
- 你的产品能容忍多久的数据不一致?(比如发券延迟5分钟行不行?)
- 团队有没有人力维护复杂的事务框架?
- 下游服务是否支持幂等和补偿?
如果答案是否定的,那就从最简单的本地消息表开始。分布式事务不是技术秀场,而是产品稳定性的底线。
最后一点思考
从测试转开发三年,我越来越觉得:最好的开发,是带着产品思维写代码的人。
测试教会我“用户视角”——用户不在乎你用了TCC还是Saga,只在乎付了钱能不能拿到东西。
而远程办公让我明白:技术方案没有高下,只有合不合适。省下的房租,不该用来买昂贵的中间件许可证,而该用来买时间,去打磨真正影响用户体验的细节。
现在,我的监控面板上,“事务异常率”稳定在0.02%以下。虽然偶尔还会半夜被报警叫醒,但至少,我能安心吃完那串烤串再处理。
共勉。

评论 0