Skip to content

分布式事务

基础理论

什么是分布式事务?

答案: 分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

典型场景

  • 跨数据库的数据一致性
  • 跨服务的业务一致性
  • 消息队列与数据库的一致性

示例

订单服务:创建订单
库存服务:扣减库存
积分服务:增加积分

要么全部成功,要么全部失败

CAP理论?

答案:

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

  • C (Consistency):一致性,所有节点同一时间看到相同数据
  • A (Availability):可用性,每个请求都能得到响应
  • P (Partition Tolerance):分区容错性,系统在网络分区时仍能工作

权衡

  • CP:保证一致性和分区容错,牺牲可用性(如Zookeeper)
  • AP:保证可用性和分区容错,牺牲一致性(如Eureka)
  • CA:保证一致性和可用性,无法容忍分区(单机系统)

分布式系统必须选择P,因此只能在C和A之间权衡。

BASE理论?

答案:

BASE是对CAP中一致性和可用性权衡的结果:

  • BA (Basically Available):基本可用,允许损失部分可用性
  • S (Soft State):软状态,允许系统中数据存在中间状态
  • E (Eventually Consistent):最终一致性,经过一段时间后达到一致

核心思想: 通过牺牲强一致性来获得可用性,系统允许在一段时间内不一致,但最终会达到一致状态。

2PC(两阶段提交)

2PC的原理?

答案:

角色

  • 协调者(Coordinator):事务管理器
  • 参与者(Participant):资源管理器

两个阶段

阶段1:准备阶段(Prepare)

1. 协调者向所有参与者发送Prepare请求
2. 参与者执行事务操作,写redo和undo日志
3. 参与者向协调者返回Yes或No

阶段2:提交阶段(Commit)

如果所有参与者都返回Yes:
  1. 协调者向所有参与者发送Commit请求
  2. 参与者提交事务,释放资源
  3. 参与者向协调者返回Ack
  4. 协调者收到所有Ack,事务完成

如果任意参与者返回No:
  1. 协调者向所有参与者发送Rollback请求
  2. 参与者回滚事务,释放资源
  3. 参与者向协调者返回Ack
  4. 协调者收到所有Ack,事务回滚

流程图

协调者                参与者A              参与者B
  |                     |                    |
  |---Prepare---------->|                    |
  |---Prepare---------------------------->|
  |                     |                    |
  |<--Yes---------------|                    |
  |<--Yes---------------------------------|
  |                     |                    |
  |---Commit----------->|                    |
  |---Commit----------------------------->|
  |                     |                    |
  |<--Ack---------------|                    |
  |<--Ack---------------------------------|

2PC的优缺点?

答案:

优点

  • 强一致性
  • 实现简单

缺点

  1. 同步阻塞:参与者在等待协调者指令时会阻塞
  2. 单点故障:协调者宕机导致参与者一直阻塞
  3. 数据不一致:Commit阶段网络分区可能导致部分提交部分未提交
  4. 性能问题:资源锁定时间长

3PC(三阶段提交)

3PC的原理?

答案:

3PC是2PC的改进版,将Prepare阶段拆分为CanCommit和PreCommit。

三个阶段

阶段1:CanCommit

1. 协调者询问参与者是否可以执行事务
2. 参与者返回Yes或No(不执行事务)

阶段2:PreCommit

如果所有参与者返回Yes:
  1. 协调者发送PreCommit请求
  2. 参与者执行事务,写日志,但不提交
  3. 参与者返回Ack

如果任意参与者返回No:
  1. 协调者发送Abort请求
  2. 事务中断

阶段3:DoCommit

如果所有参与者返回Ack:
  1. 协调者发送DoCommit请求
  2. 参与者提交事务
  3. 参与者返回Ack

如果超时或收到Abort:
  1. 参与者回滚事务

改进

  • 引入超时机制,参与者超时自动提交
  • 减少阻塞时间

缺点

  • 仍然存在数据不一致风险
  • 实现复杂

TCC(Try-Confirm-Cancel)

TCC的原理?

答案:

TCC是一种补偿型事务,将事务分为三个阶段:

三个阶段

1. Try阶段

  • 尝试执行业务
  • 完成所有业务检查
  • 预留必须的业务资源

2. Confirm阶段

  • 确认执行业务
  • 不做任何业务检查
  • 只使用Try阶段预留的资源

3. Cancel阶段

  • 取消执行业务
  • 释放Try阶段预留的资源

示例:转账业务

java
// Try阶段:冻结资金
public interface AccountService {
    @Compensable(
        confirmMethod = "confirmTransfer",
        cancelMethod = "cancelTransfer"
    )
    void tryTransfer(String from, String to, BigDecimal amount);

    void confirmTransfer(String from, String to, BigDecimal amount);

    void cancelTransfer(String from, String to, BigDecimal amount);
}

@Service
public class AccountServiceImpl implements AccountService {

    @Override
    public void tryTransfer(String from, String to, BigDecimal amount) {
        // 1. 检查余额是否足够
        // 2. 冻结from账户的amount金额
        // 3. 记录冻结记录
    }

    @Override
    public void confirmTransfer(String from, String to, BigDecimal amount) {
        // 1. 扣减from账户冻结金额
        // 2. 增加to账户余额
        // 3. 删除冻结记录
    }

    @Override
    public void cancelTransfer(String from, String to, BigDecimal amount) {
        // 1. 解冻from账户金额
        // 2. 删除冻结记录
    }
}

TCC的优缺点?

答案:

优点

  • 性能较好,不长时间锁定资源
  • 数据最终一致性
  • 不依赖数据库事务

缺点

  • 业务侵入性强,需要实现Try、Confirm、Cancel三个方法
  • 开发成本高
  • 需要考虑幂等性、空回滚、悬挂等问题

注意事项

  1. 幂等性:Confirm和Cancel可能被重复调用
  2. 空回滚:Try未执行,Cancel被调用
  3. 悬挂:Cancel先于Try执行

Saga模式

Saga的原理?

答案:

Saga将长事务拆分为多个本地短事务,每个短事务都有对应的补偿事务。

两种实现方式

1. 事件驱动(Choreography)

服务A执行 → 发布事件 → 服务B监听事件执行
         ↓ 失败
      发布补偿事件 → 服务A监听事件补偿

2. 命令协调(Orchestration)

协调器 → 调用服务A → 成功 → 调用服务B → 成功 → 完成
                  ↓ 失败          ↓ 失败
               补偿服务A        补偿服务B → 补偿服务A

示例:订单流程

java
// 正向流程
createOrder() → reduceStock() → deductBalance() → sendMessage()

// 补偿流程(任意步骤失败)
cancelOrder() ← addStock() ← refundBalance() ← cancelMessage()

代码示例

java
@Service
public class OrderSaga {

    @Autowired
    private OrderService orderService;
    @Autowired
    private StockService stockService;
    @Autowired
    private AccountService accountService;

    public void createOrder(Order order) {
        try {
            // 1. 创建订单
            orderService.create(order);

            // 2. 扣减库存
            stockService.reduce(order.getProductId(), order.getQuantity());

            // 3. 扣减余额
            accountService.deduct(order.getUserId(), order.getAmount());

            // 4. 发送消息
            messageService.send(order);

        } catch (Exception e) {
            // 补偿操作
            compensate(order);
        }
    }

    private void compensate(Order order) {
        // 按相反顺序补偿
        messageService.cancel(order);
        accountService.refund(order.getUserId(), order.getAmount());
        stockService.add(order.getProductId(), order.getQuantity());
        orderService.cancel(order.getId());
    }
}

Saga的优缺点?

答案:

优点

  • 一阶段提交,性能好
  • 无长时间锁定资源
  • 适合长流程业务

缺点

  • 不保证隔离性
  • 需要实现补偿逻辑
  • 补偿可能失败

本地消息表

本地消息表的原理?

答案:

通过本地事务保证业务操作和消息发送的一致性。

流程

1. 开启本地事务
2. 执行业务操作
3. 插入消息表
4. 提交本地事务
5. 定时任务扫描消息表
6. 发送消息到MQ
7. 消费者消费消息
8. 更新消息状态为已发送

代码示例

java
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private MessageMapper messageMapper;

    @Transactional
    public void createOrder(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);

        // 2. 插入消息表
        Message message = new Message();
        message.setContent(JSON.toJSONString(order));
        message.setStatus(0);  // 待发送
        messageMapper.insert(message);

        // 3. 本地事务提交
    }
}

// 定时任务
@Scheduled(fixedDelay = 5000)
public void sendMessage() {
    List<Message> messages = messageMapper.selectPending();
    for (Message message : messages) {
        try {
            // 发送到MQ
            mqProducer.send(message.getContent());
            // 更新状态
            message.setStatus(1);  // 已发送
            messageMapper.update(message);
        } catch (Exception e) {
            // 记录日志,下次重试
        }
    }
}

数据库表

sql
CREATE TABLE message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    content TEXT,
    status INT,  -- 0:待发送 1:已发送 2:失败
    retry_count INT DEFAULT 0,
    create_time DATETIME,
    update_time DATETIME
);

本地消息表的优缺点?

答案:

优点

  • 实现简单
  • 不依赖第三方组件
  • 消息一定会发送

缺点

  • 业务耦合,需要创建消息表
  • 消息可能重复发送,需要保证幂等性
  • 定时任务有延迟

可靠消息最终一致性

可靠消息最终一致性的原理?

答案:

通过消息中间件保证消息的可靠投递。

RocketMQ事务消息

1. 发送Half消息(半消息)
2. 执行本地事务
3. 提交或回滚消息
4. 消费者消费消息
5. MQ定时回查事务状态

代码示例

java
@Service
public class OrderService implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderMapper orderMapper;

    // 发送事务消息
    public void createOrder(Order order) {
        rocketMQTemplate.sendMessageInTransaction(
            "order-topic",
            MessageBuilder.withPayload(order).build(),
            order
        );
    }

    // 执行本地事务
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            Order order = (Order) arg;
            orderMapper.insert(order);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    // 事务回查
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        // 查询订单是否存在
        Order order = orderMapper.selectById(orderId);
        if (order != null) {
            return RocketMQLocalTransactionState.COMMIT;
        } else {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}

// 消费者
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "stock-consumer")
public class StockConsumer implements RocketMQListener<Order> {

    @Override
    public void onMessage(Order order) {
        // 扣减库存(需要保证幂等性)
        stockService.reduce(order.getProductId(), order.getQuantity());
    }
}

如何保证消息的幂等性?

答案:

方案1:唯一ID + 去重表

java
@Transactional
public void consume(Message message) {
    String messageId = message.getId();

    // 检查是否已消费
    if (consumeRecordMapper.exists(messageId)) {
        return;  // 已消费,直接返回
    }

    // 执行业务逻辑
    stockService.reduce(productId, quantity);

    // 记录消费记录
    consumeRecordMapper.insert(messageId);
}

方案2:业务唯一键

java
// 使用订单号作为唯一键
UPDATE stock SET quantity = quantity - 1
WHERE product_id = ? AND quantity >= 1

方案3:状态机

java
// 只有待支付状态才能变为已支付
UPDATE order SET status = 'PAID'
WHERE order_id = ? AND status = 'PENDING'

Seata

Seata的工作模式?

答案:

Seata支持四种模式:

1. AT模式(自动)

  • 自动生成反向SQL
  • 无业务侵入
  • 适合大部分场景

2. TCC模式

  • 需要实现Try、Confirm、Cancel
  • 性能较好
  • 业务侵入性强

3. Saga模式

  • 适合长流程业务
  • 需要实现补偿逻辑

4. XA模式

  • 基于数据库XA协议
  • 强一致性
  • 性能较差

AT模式示例

java
@GlobalTransactional
public void createOrder(Order order) {
    // 1. 创建订单
    orderService.create(order);

    // 2. 扣减库存(远程调用)
    stockService.reduce(order.getProductId(), order.getQuantity());

    // 3. 扣减余额(远程调用)
    accountService.deduct(order.getUserId(), order.getAmount());
}

配置

yaml
seata:
  tx-service-group: my-tx-group
  service:
    vgroup-mapping:
      my-tx-group: default
    grouplist:
      default: 127.0.0.1:8091

练习题

  1. 如何选择合适的分布式事务方案?
  2. 如何处理分布式事务的超时问题?
  3. 如何保证分布式事务的性能?
  4. 分布式事务和本地事务的区别?
  5. 如何测试分布式事务的正确性?

Released under the MIT License.