从外包到自研:我在分布式事务里踩过的那些坑

机器学习厨子
2026-08-16 23:41
阅读 411

起因:一笔订单把库存扣成了负数

去年双十一预热,订单量翻了五倍,运营发截图:某商品库存显示-3。超卖了。

查了半天,问题出在订单服务和库存服务之间。简化伪代码:

@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    inventoryService.deductStock(order.getSkuId(), order.getQuantity());
}

deductStock走HTTP调用,库存服务自己也有事务。库存扣减成功、订单插入失败,或反过来,数据就不一致了。网络超时更恶心,你根本不知道对面执行成功没有。

我用Bolt.new搭了最小复现demo,模拟各种失败场景。结论:本地事务管不了远程调用,这是分布式事务问题的根源

第一坑:迷信2PC,把自己锁死了

一开始用2PC,选了开源XA实现,把订单库和库存库都接进去。压测QPS直接掉了一半多。原因:XA协议在准备阶段持有数据库锁,协调者挂了,参与者全卡住。一次网络抖动,订单库连接池被占满,服务雪崩。

业务上根本不需要那么强的实时一致性。库存晚扣几秒用户无感知。为了99.9%不会发生的极端场景牺牲50%性能,是过度设计。后来整个方案推翻了。

转折:用本地消息表实现最终一致性

电商下单的核心诉求:订单一定要创建成功,库存最终一定要扣减,中间允许短暂不一致。这就是最终一致性。

我选了本地消息表方案。思路:把远程调用变成数据库记录,用后台任务不断重试。

  1. outbox_event表:
CREATE TABLE outbox_event (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(64) NOT NULL,
    payload JSON NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,
    retry_count INT NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);
  1. 创建订单时,同一本地事务里插入订单和事件记录:
@Transactional
public void createOrder(Order order) {
    orderMapper.insert(order);
    OutboxEvent event = new OutboxEvent();
    event.setEventType("ORDER_CREATED");
    event.setPayload(JSON.toJSONString(order));
    outboxEventMapper.insert(event);
}
  1. 定时任务每5秒扫描status=0的记录发给库存服务,成功标记1,失败重试。

核心:订单和事件在同一本地事务里,要么都成功要么都失败。库存服务要做幂等,根据订单号去重。上线后超卖解决,性能几乎无损耗。

第二坑:幂等没做好,重复扣库存

上线第三天,有用户买一件,库存扣了两件。原因是定时任务重试:第一次调用库存服务成功但响应超时,任务以为失败又重试,扣了两次。

最终一致性方案里重试是常态,消费方必须幂等。我在库存服务加了deduct_log表,扣减前先查订单号,存在就跳过:

public void deductStock(String orderNo, String skuId, int quantity) {
    if (deductLogMapper.existsByOrderNo(orderNo)) {
        return;
    }
    stockMapper.deduct(skuId, quantity);
    deductLogMapper.insert(orderNo, skuId, quantity);
}

扣减和记录日志必须在同一本地事务里,用唯一索引(order_no)兜底,并发进来第二个插入失败,整个事务回滚。幂等是分布式系统的基本素养

第三坑:事务消息的"假成功"

订单量继续涨,本地消息表轮询延迟明显。改用RocketMQ事务消息,相当于把本地消息表逻辑内置到中间件。

但事务消息有新坑:本地事务执行时间太长,MQ会回调检查状态,而代码可能还没执行完。我遇到订单创建涉及风控校验,耗时超过回查间隔,MQ查不到订单就回滚消息,结果订单创建成功但库存没扣。

解决:把回查逻辑和本地事务状态绑定,先插入事务状态记录,回查时根据状态返回。事务消息不是银弹,只是把重试逻辑搬到中间件里,该处理的边界情况一个不少

关于Google Cloud和Bolt.new

我习惯先用Bolt.new快速搭原型验证思路,再改公司代码。输入需求直接生成可运行项目骨架,半小时就能验证核心逻辑。

公司后来把定时任务迁移到Cloud Scheduler,消息队列用Pub/Sub。云厂商托管服务屏蔽了底层复杂度,但不代表可以不理解原理。Pub/Sub的at-least-once投递语义,意味着消费端必须幂等——和前面踩的坑完全一回事。

总结:分布式事务没有银弹

分布式事务的本质不是技术选型问题,而是业务权衡问题

推荐方案:

  • 能避免分布式事务就避免。通过业务设计把操作收敛到一个服务,或用本地事务加补偿。
  • 需要最终一致性时,本地消息表最务实。简单、可控、不依赖特定中间件。
  • 2PC/XA能不用就不用。性能损耗大,运维要求高。
  • 无论什么方案,幂等设计是底线。没有幂等,任何重试都是灾难。

从机械厂画图到写分布式事务,我花了三年。30岁转行不算晚,但要付出更多。踩坑是正常的,关键是每次能爬出来,并记住坑在哪儿。

评论 0

最热最新
暂无评论
机器学习厨子Lv.1
0
影响力
0
文章
0
粉丝