分布式事务解决方案:最佳实践(一个被Cursor惯坏的后端仔的血泪总结)

Django老掌柜
2025-12-16 06:38
阅读 279

成都的夏天闷热得离谱,但好在我是早起型选手——每天8点准时坐到工位上,咖啡还没凉,代码就已经跑起来了。自从去年彻底投奔 Cursor 的怀抱,写代码这件事对我而言已经从“搬砖”进化成了“指挥AI打工”。说真的,现在让我手动敲完一个完整的 Service 层,手都会抖。

不过,AI 再聪明,也救不了架构设计上的坑。上周五晚上 10 点,我还在公司死磕一个线上分布式事务问题,产品经理在群里疯狂@:“双11预热活动明天上线,这个订单状态不一致的问题必须今晚搞定!” 我盯着屏幕里那条诡异的 OrderStatus = PAIDInventoryService 显示库存未扣减的日志,内心万马奔腾——这哪是写代码,这是在玩火。

今天这篇文章,就是我用血泪换来的分布式事务实战笔记。不讲理论堆砌,只聊怎么在真实业务中把性能、稳定性、可维护性都拉满。顺便安利几本我看过的神书,以及为什么我觉得“代码人生”不该只是 CRUD。


背景:我们到底在怕什么?

先说清楚场景。我们团队做的是一个电商聚合平台,后端服务拆得比较细:

  • OrderService:处理下单、支付回调
  • InventoryService:管理商品库存
  • CouponService:核销优惠券
  • LogisticsService:生成物流单

典型的“一笔订单,四个服务”的微服务架构。用户支付成功后,OrderService 收到支付宝回调,然后依次调用其他三个服务。听起来很顺?现实是:网络会超时、服务会宕机、数据库会主从延迟

去年双11,我们就因为 InventoryService 短暂不可用,导致 Order 状态变成 PAID,但库存没扣。结果就是——超卖。客服电话被打爆,老板脸色比成都的雾霾还黑。

这时候你可能会说:“用两阶段提交(2PC)啊!” 拜托,2PC 在高并发下就是性能杀手,锁表时间长到能让你泡三杯茶。而且我们的 MySQL 集群扛不住这种全局锁。

所以问题核心就两个:

  1. 如何保证多个服务的数据最终一致?
  2. 如何在不牺牲吞吐量的前提下做到这一点?

方案选型:别被论文忽悠瘸了

我翻过《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_key hash,保证同一订单的消息顺序

坑与填坑:那些让我想砸键盘的瞬间

坑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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝