在微服务架构中,业务逻辑拆分到多个服务,跨服务数据操作(如订单创建 + 库存扣减 + 支付扣钱)成为常态,传统单机事务(ACID)无法满足跨服务数据一致性需求,易出现 “部分成功、部分失败” 的脏数据问题(如订单创建成功但库存未扣减)。

本文从分布式事务核心理论出发,拆解 2PC、TCC、SAGA、本地消息表、可靠消息队列五大主流方案,分析适用场景、实现逻辑与优缺点,结合代码示例给出落地方案,帮你在不同业务场景下选择最优一致性方案。

一、核心认知:分布式事务的挑战与核心理论

1. 核心挑战

  • 跨服务通信不确定性:网络延迟、服务宕机导致事务参与者状态不一致;
  • 数据隔离难度高:多服务并发操作数据,难以保证单机事务的隔离性(如脏读、不可重复读);
  • 性能与一致性平衡:强一致性方案(如 2PC)性能差,最终一致性方案需容忍短暂不一致。

2. 核心理论(CAP 与 BASE)

(1)CAP 定理

分布式系统中,三个特性无法同时满足:

  • 一致性(Consistency):所有节点数据实时一致;
  • 可用性(Availability):服务始终可用,无响应超时;
  • 分区容错性(Partition tolerance):网络分区时,服务仍能正常运行。
  • 实际落地:微服务优先保证AP(可用性 + 分区容错性),采用最终一致性方案;金融核心场景(如转账)可牺牲部分可用性,保证强一致性。
(2)BASE 理论

对 CAP 定理的补充,面向最终一致性:

  • 基本可用(Basically Available):服务降级可用(如返回默认数据、限流);
  • 软状态(Soft State):允许数据短暂不一致;
  • 最终一致性(Eventually Consistent):一段时间后,数据自动同步为一致状态。

3. 分布式事务核心需求

  • 原子性:跨服务操作要么全成功,要么全回滚;
  • 最终一致性:允许短暂不一致,但需在规定时间内同步一致;
  • 高性能:尽量减少事务对接口响应速度的影响;
  • 高可用:避免事务协调器 / 参与者宕机导致整体不可用。

二、实战:五大分布式事务方案落地

1. 方案一:2PC(两阶段提交,强一致性)

(1)核心原理

分为准备阶段(Prepare)和提交阶段(Commit),由事务协调器(TC)统一管理参与者(TM):

  1. 准备阶段:TC 通知所有参与者执行操作但不提交,参与者执行成功后返回 “就绪”,失败返回 “ abort”;
  2. 提交阶段:若所有参与者均就绪,TC 通知所有参与者提交;若有任一参与者失败,TC 通知所有参与者回滚。
(2)优缺点与适用场景
  • 优点:强一致性,实现简单;
  • 缺点:性能差(两阶段阻塞)、容错性低(协调器宕机导致事务卡死)、不适合微服务(耦合度高);
  • 适用场景:金融核心场景(如转账)、单机多数据源场景(非微服务)。
(3)实现示例(Seata AT 模式简化版)

Seata 是阿里开源分布式事务框架,支持 2PC 的 AT 模式(自动补偿):

  1. 引入依赖:

xml

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.1</version>
</dependency>
  1. 配置 Seata(registry.conf/application.yml):

yaml

seata:
  tx-service-group: my_test_tx_group # 事务组名称
  registry:
    type: nacos # 注册中心(与微服务一致)
    nacos:
      server-addr: 127.0.0.1:8848
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
  1. 业务代码(订单服务调用库存服务):

java

运行

// 订单服务(发起者)
@Service
public class OrderService {
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StockFeignClient stockFeignClient; // 库存服务Feign客户端

    @GlobalTransactional(rollbackFor = Exception.class) // Seata全局事务注解
    public void createOrder(CreateOrderRequest request) {
        // 1. 订单服务:创建订单
        OrderDO orderDO = new OrderDO();
        orderDO.setUserId(request.getUserId());
        orderDO.setProductId(request.getProductId());
        orderDO.setCount(request.getCount());
        orderMapper.insert(orderDO);

        // 2. 调用库存服务:扣减库存(跨服务调用)
        StockDeductRequest deductRequest = new StockDeductRequest();
        deductRequest.setProductId(request.getProductId());
        deductRequest.setDeductCount(request.getCount());
        Result<Boolean> deductResult = stockFeignClient.deductStock(deductRequest);
        if (!deductResult.isSuccess()) {
            throw new BusinessException("库存扣减失败");
        }
    }
}

// 库存服务(参与者)
@RestController
@RequestMapping("/stocks")
public class StockController {
    @Resource
    private StockMapper stockMapper;

    @PostMapping("/deduct")
    public Result<Boolean> deductStock(@RequestBody StockDeductRequest request) {
        // 扣减库存(Seata自动记录undo_log,异常时回滚)
        int rows = stockMapper.deductStock(request.getProductId(), request.getDeductCount());
        return rows > 0 ? Result.success(true) : Result.fail(40001, "库存不足");
    }
}

⚠️ 关键:Seata AT 模式通过自动生成 undo_log 日志、代理数据源实现自动提交 / 回滚,对业务侵入极低。

2. 方案二:TCC(补偿事务,最终一致性)

(1)核心原理

TCC(Try-Confirm-Cancel)是基于业务层补偿的方案,分为三个阶段,无事务协调器,由业务代码控制:

  1. Try 阶段:预留资源(如锁定库存、冻结余额),确保后续操作可执行;
  2. Confirm 阶段:确认执行(如扣减预留库存、扣减冻结余额),仅在 Try 成功后执行;
  3. Cancel 阶段:补偿回滚(如释放预留库存、解冻余额),Try 失败或超时后执行。
(2)优缺点与适用场景
  • 优点:性能高(无锁阻塞)、灵活性强(适配复杂业务)、容错性好;
  • 缺点:业务侵入性强(需手动实现 Try/Confirm/Cancel)、需处理幂等性(避免重复补偿);
  • 适用场景:高并发微服务场景(如电商下单、支付)、对性能要求高的最终一致性场景。
(3)实现示例(订单 + 库存 TCC)

java

运行

// 库存服务TCC接口(定义三阶段方法)
public interface StockTccService {
    // Try:预留库存
    boolean tryDeductStock(Long productId, Integer count);
    // Confirm:确认扣减库存
    boolean confirmDeductStock(Long productId, Integer count);
    // Cancel:释放预留库存
    boolean cancelDeductStock(Long productId, Integer count);
}

// 库存服务实现
@Service
public class StockTccServiceImpl implements StockTccService {
    @Resource
    private StockMapper stockMapper;

    @Override
    public boolean tryDeductStock(Long productId, Integer count) {
        // 预留库存:扣减可用库存,增加预留库存
        return stockMapper.reserveStock(productId, count) > 0;
    }

    @Override
    public boolean confirmDeductStock(Long productId, Integer count) {
        // 确认扣减:扣减预留库存
        return stockMapper.confirmReserveStock(productId, count) > 0;
    }

    @Override
    public boolean cancelDeductStock(Long productId, Integer count) {
        // 取消扣减:释放预留库存(加回可用库存)
        return stockMapper.releaseReserveStock(productId, count) > 0;
    }
}

// 订单服务调用(手动控制TCC三阶段)
@Service
public class OrderTccService {
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StockTccService stockTccService;
    @Resource
    private ScheduledExecutorService scheduledExecutorService;

    public void createOrder(CreateOrderRequest request) {
        Long productId = request.getProductId();
        Integer count = request.getCount();
        boolean success = false;

        try {
            // 1. Try阶段:预留库存
            boolean trySuccess = stockTccService.tryDeductStock(productId, count);
            if (!trySuccess) {
                throw new BusinessException("库存不足,预留失败");
            }

            // 2. 执行核心业务:创建订单
            OrderDO orderDO = new OrderDO();
            // 订单信息赋值...
            orderMapper.insert(orderDO);

            // 3. Confirm阶段:确认扣减库存
            boolean confirmSuccess = stockTccService.confirmDeductStock(productId, count);
            if (confirmSuccess) {
                success = true;
                return;
            }
        } catch (Exception e) {
            log.error("订单创建异常", e);
        }

        // 4. 异常/Confirm失败:执行Cancel阶段补偿
        if (!success) {
            // 异步重试Cancel(避免阻塞)
            scheduledExecutorService.submit(() -> {
                int retryCount = 3;
                while (retryCount-- > 0) {
                    try {
                        if (stockTccService.cancelDeductStock(productId, count)) {
                            break;
                        }
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        log.error("Cancel库存失败,重试次数:{}", retryCount, e);
                    }
                }
            });
            throw new BusinessException("订单创建失败,已补偿库存");
        }
    }
}

⚠️ 关键:TCC 需处理幂等性(如用订单 ID 作为唯一标识,避免重复 Confirm/Cancel)、超时重试(通过定时任务重试失败的补偿操作)。

3. 方案三:本地消息表(最终一致性,低侵入)

(1)核心原理

基于 “本地事务 + 消息队列”,将跨服务事务转化为单服务本地事务,通过消息异步同步数据:

  1. 本地事务:在发起者服务中,将 “业务操作” 与 “写入消息表” 放在同一本地事务;
  2. 消息发送:通过定时任务扫描消息表,将未发送的消息投递到 MQ;
  3. 消息消费:接收者服务消费 MQ 消息,执行对应业务操作,消费成功后更新消息状态;
  4. 重试机制:消费失败时,MQ 重试投递,确保消息最终被消费。
(2)优缺点与适用场景
  • 优点:业务侵入性低、实现简单、容错性好(消息可重试);
  • 缺点:依赖消息队列、一致性延迟高(受定时任务频率影响);
  • 适用场景:非实时一致性场景(如订单创建后发送通知、日志同步)、中小规模微服务。
(3)实现示例(订单创建 + 消息通知)
  1. 订单服务创建本地消息表(与订单表同库):

sql

CREATE TABLE `order_message` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `order_id` bigint NOT NULL COMMENT '订单ID',
  `message_content` varchar(500) NOT NULL COMMENT '消息内容',
  `message_status` tinyint NOT NULL DEFAULT 0 COMMENT '消息状态:0-未发送,1-已发送,2-已消费',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`),
  KEY `idx_message_status` (`message_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '订单本地消息表';
  1. 订单服务实现(本地事务 + 消息发送):

java

运行

// 订单服务
@Service
public class OrderMessageService {
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private OrderMessageMapper messageMapper;
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private ScheduledExecutorService scheduledExecutorService;

    // 初始化定时任务:每10秒扫描未发送消息
    @PostConstruct
    public void initMessageSender() {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            // 扫描未发送消息
            List<OrderMessage> unSentMessages = messageMapper.selectByStatus(0);
            for (OrderMessage message : unSentMessages) {
                try {
                    // 发送消息到MQ
                    rabbitTemplate.convertAndSend("order.notify.exchange", "order.create", message.getOrderId());
                    // 更新消息状态为已发送
                    message.setMessageStatus(1);
                    message.setUpdateTime(LocalDateTime.now());
                    messageMapper.updateById(message);
                } catch (Exception e) {
                    log.error("消息发送失败,订单ID:{}", message.getOrderId(), e);
                }
            }
        }, 0, 10, TimeUnit.SECONDS);
    }

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        // 1. 本地事务:创建订单+写入消息表
        OrderDO orderDO = new OrderDO();
        // 订单信息赋值...
        orderMapper.insert(orderDO);

        // 写入消息表(与订单创建在同一事务,要么都成功,要么都回滚)
        OrderMessage message = new OrderMessage();
        message.setOrderId(orderDO.getId());
        message.setMessageContent(JSON.toJSONString(orderDO));
        message.setMessageStatus(0);
        message.setCreateTime(LocalDateTime.now());
        message.setUpdateTime(LocalDateTime.now());
        messageMapper.insert(message);
    }
}
  1. 通知服务消费消息:

java

运行

// 通知服务
@Service
public class NotifyService {
    @Resource
    private OrderMessageMapper messageMapper; // 跨库查询消息表(或冗余消息状态)

    @RabbitListener(queues = "order.create.queue")
    public void handleOrderCreate(Long orderId) {
        try {
            // 1. 执行通知业务(如发送短信、推送)
            sendSmsToUser(orderId);
            sendPushToUser(orderId);

            // 2. 更新消息状态为已消费
            OrderMessage message = messageMapper.selectByOrderId(orderId);
            if (message != null) {
                message.setMessageStatus(2);
                message.setUpdateTime(LocalDateTime.now());
                messageMapper.updateById(message);
            }
        } catch (Exception e) {
            log.error("订单通知失败,订单ID:{}", orderId, e);
            // 抛出异常,MQ自动重试(默认3次,可配置重试次数)
            throw new AmqpRejectAndDontRequeueException("通知失败,触发重试");
        }
    }

    private void sendSmsToUser(Long orderId) {
        // 发送短信逻辑...
    }

    private void sendPushToUser(Long orderId) {
        // 发送推送逻辑...
    }
}

4. 方案四:可靠消息队列(RocketMQ 事务消息,最终一致性)

(1)核心原理

RocketMQ 原生支持事务消息,简化本地消息表方案,由 MQ 负责消息的可靠性投递与状态回查:

  1. 发送半事务消息:发起者发送 “半事务消息” 到 MQ,MQ 标记消息为 “待确认”;
  2. 执行本地事务:发起者执行本地业务操作(如创建订单);
  3. 确认消息状态:本地事务成功则提交消息(MQ 将消息改为 “可消费”),失败则回滚消息(MQ 删除消息);
  4. 消息回查:若 MQ 未收到确认,定时回查发起者本地事务状态,确保消息状态一致。
(2)优缺点与适用场景
  • 优点:无本地消息表、业务侵入低、可靠性高(MQ 保障);
  • 缺点:依赖 RocketMQ(不支持 Kafka)、一致性延迟受回查频率影响;
  • 适用场景:主流微服务场景(如订单、支付、库存)、不想维护本地消息表的场景。

5. 方案五:SAGA(长事务补偿,最终一致性)

(1)核心原理

SAGA 将长事务拆分为多个短事务(每个短事务对应一个服务操作),每个短事务执行后记录补偿操作,若某一步失败,反向执行所有已完成短事务的补偿操作:

  1. 正向流程:T1(服务 1 操作)→ T2(服务 2 操作)→ ... → Tn(服务 n 操作);
  2. 补偿流程:若 Tk 失败,执行 Ck-1(Tk-1 补偿)→ ... → C1(T1 补偿)。
(2)优缺点与适用场景
  • 优点:适配长事务(如订单履约、物流调度)、无锁阻塞、灵活性强;
  • 缺点:业务侵入性强(需设计补偿操作)、一致性延迟高;
  • 适用场景:长事务场景(如跨多服务的订单履约流程)、复杂业务流程的最终一致性需求。

三、方案对比与选型建议

方案 一致性 性能 业务侵入性 适用场景 依赖
2PC(Seata AT) 强一致性 中低 金融核心、单机多数据源 事务协调器(Seata)
TCC 最终一致性 电商高并发、高性能需求 无(手动实现补偿)
本地消息表 最终一致性 中小规模、非实时场景 消息队列 + 定时任务
可靠消息队列 最终一致性 中高 主流微服务、无本地表需求 RocketMQ
SAGA 最终一致性 中高 长事务、复杂业务流程 无(需设计补偿链)

选型核心原则

  1. 金融核心场景(转账、支付):选 2PC(Seata AT),保证强一致性;
  2. 电商高并发场景(下单、库存):选 TCC 或可靠消息队列,平衡性能与一致性;
  3. 非实时场景(通知、日志):选本地消息表,实现简单、低侵入;
  4. 长事务场景(履约、调度):选 SAGA,适配多步骤复杂流程。

四、避坑指南

1. 坑点 1:忽视幂等性处理

  • 表现:消息重试、补偿操作导致重复执行(如重复扣减库存、重复创建订单);
  • 解决方案:用唯一标识(订单 ID、消息 ID)实现幂等,如数据库唯一索引、Redis 缓存去重。

2. 坑点 2:补偿操作设计不当

  • 表现:补偿操作失败(如释放库存失败),导致数据永久不一致;
  • 解决方案:补偿操作设计为 “幂等、可重试”,通过定时任务重试失败的补偿操作,记录补偿日志便于排查。

3. 坑点 3:过度追求强一致性

  • 表现:所有场景都用 2PC,导致接口性能差、可用性低;
  • 解决方案:非核心场景优先选最终一致性方案,平衡性能与一致性需求。

4. 坑点 4:未处理超时场景

  • 表现:服务调用超时、消息投递超时,导致事务状态未知;
  • 解决方案:设置合理超时时间,超时触发补偿操作,通过定时任务回查事务状态。

5. 坑点 5:依赖单点组件

  • 表现:事务协调器(Seata)、消息队列单点故障,导致分布式事务不可用;
  • 解决方案:部署组件集群(Seata 集群、RocketMQ 集群),保证高可用。

五、终极总结:分布式事务的核心是 “场景适配”

分布式事务没有 “银弹方案”,核心是根据业务场景选择适配的方案 —— 强一致性场景牺牲性能保一致,高并发场景牺牲短暂一致性保性能。

落地时需记住:

  1. 优先最终一致性:绝大多数微服务场景无需强一致性,最终一致性足以满足需求;
  2. 低侵入优先:优先选 Seata AT、可靠消息队列,减少业务代码侵入;
  3. 做好容错机制:幂等、重试、补偿、日志缺一不可,确保异常场景可恢复;
  4. 性能与一致平衡:避免过度设计,在业务可接受的延迟内实现一致性。
Logo

更多推荐