本地事务与分布式事务:从原理到实践

引言

在现代软件系统中,事务处理是保证数据一致性和可靠性的核心机制。从早期的单机数据库到如今的大规模分布式系统,事务处理技术经历了深刻的演进。


第一部分:历史背景与演进

1.1 事务概念的起源

事务(Transaction)的概念最早诞生于20世纪70年代的数据库领域。1983年,Andreas Reuter 和 Theo Härder 在论文中正式提出了ACID理论,为事务处理奠定了理论基础。

ACID的诞生背景:在早期的银行系统中,工程师们发现了一个严重问题:当系统在执行转账操作时突然崩溃,可能出现钱从一个账户扣除了,却没有加到另一个账户的情况。这种"部分完成"的操作会导致数据不一致,造成严重的业务损失。

为了解决这个问题,数据库先驱们提出:将一组相关的操作捆绑成一个"不可分割的工作单元",要么全部成功,要么全部失败。这就是事务的核心思想。

1.2 从单体到分布式的演进历程

第一阶段:单机数据库时代(1970s-1990s)

  • 所有数据存储在一台数据库服务器上
  • 事务由数据库管理系统(DBMS)内部控制
  • 通过锁机制和预写日志(WAL)保证ACID特性
  • 典型代表:Oracle、DB2、Sybase

第二阶段:分布式数据库萌芽(1990s-2000s)

  • 业务增长导致单机数据库性能瓶颈
  • 出现数据分片和主从复制架构
  • 引入两阶段提交(2PC)协议处理跨库事务
  • 典型代表:早期的分布式数据库系统

第三阶段:微服务与分布式系统爆发(2010s-至今)

  • SOA和微服务架构成为主流
  • 业务被拆分为多个独立服务,各自管理数据
  • 传统ACID事务难以跨服务实现
  • 出现TCC、Saga、最终一致性等新型方案
  • 典型代表:电商系统、互联网平台

1.3 为什么需要分布式事务

业务驱动因素:

  1. 业务复杂度提升

    • 一个订单流程可能涉及:订单服务、库存服务、支付服务、积分服务、物流服务
    • 每个服务独立部署,有自己的数据库
    • 需要保证整个流程的数据一致性
  2. 性能与扩展性需求

    • 单机数据库无法支撑亿级用户访问
    • 必须进行水平扩展和数据分片
    • 数据分散后,跨分片操作需要分布式事务保证
  3. 系统可用性要求

    • 微服务架构下,服务独立部署和升级
    • 部分服务故障不应影响整体系统
    • 需要柔性事务方案保证最终一致性

真实案例:某电商平台在2015年双11时,订单创建流程涉及6个微服务。使用传统2PC方案导致性能瓶颈,订单创建耗时从平时的200ms飙升至5秒,系统几乎瘫痪。最终采用Saga模式重构,将耗时降至300ms,成功支撑了峰值流量。


第二部分:核心技术原理

2.1 本地事务深度解析

2.1.1 ACID特性详解

A - Atomicity (原子性)

  • 定义:事务中的所有操作要么全部成功,要么全部失败回滚
  • 实现机制:通过Undo Log(回滚日志)实现
    • 执行操作前,先将原始数据记录到Undo Log
    • 如果事务失败,根据Undo Log恢复数据
    • 如果事务成功,清理Undo Log
  • 示例:转账操作包含两步:1)从A账户扣款 2)向B账户加款。如果第2步失败,第1步必须回滚。

C - Consistency (一致性)

  • 定义:事务执行前后,数据库从一个一致性状态转换到另一个一致性状态
  • 理解要点:一致性是目标,原子性、隔离性、持久性是手段
  • 示例:转账前后,所有账户总金额保持不变(业务规则约束)

I - Isolation (隔离性)

  • 定义:并发执行的事务之间相互隔离,互不干扰
  • 隔离级别(从弱到强):
    1. 读未提交(Read Uncommitted):可能读到其他事务未提交的数据(脏读)
    2. 读已提交(Read Committed):只能读到已提交的数据,但同一事务内多次读取可能不一致(不可重复读)
    3. 可重复读(Repeatable Read):同一事务内多次读取结果一致,但可能出现幻读
    4. 串行化(Serializable):完全隔离,性能最差

D - Durability (持久性)

  • 定义:事务一旦提交,对数据的修改永久保存,即使系统崩溃也不会丢失
  • 实现机制:通过Redo Log(重做日志)和WAL(预写日志)实现
    • 先将修改写入日志,再修改数据页
    • 系统崩溃后,根据Redo Log恢复已提交的事务
    • 即使数据页未刷盘,也能保证持久性
2.1.2 关键实现机制

1. 锁机制

锁的层次结构:
数据库级锁 → 表级锁 → 页级锁 → 行级锁

锁的类型:
- 共享锁(S锁):读锁,多个事务可同时持有
- 排他锁(X锁):写锁,独占访问
- 意向锁(IS/IX):表级锁,表示下层有共享锁或排他锁

锁的问题与解决:

  • 死锁:两个事务相互等待对方持有的锁
    • 解决:死锁检测算法,超时回滚,或按固定顺序加锁
  • 性能问题:锁竞争导致吞吐量下降
    • 解决:使用MVCC减少锁竞争

2. MVCC (多版本并发控制)

MVCC是一种优雅的并发控制机制,核心思想是为每条记录维护多个版本,读写操作在不同版本上进行,互不阻塞。

工作原理(以MySQL InnoDB为例):

记录结构:
[数据行] | [DB_TRX_ID] | [DB_ROLL_PTR] | [版本链]
         事务ID         回滚指针         指向旧版本

版本链示例:
当前版本: {id:1, name:'Alice', age:30} [TRX_ID=100]
         ↓ (DB_ROLL_PTR指向)
旧版本1: {id:1, name:'Alice', age:25} [TRX_ID=80]
         ↓
旧版本2: {id:1, name:'Alice', age:20} [TRX_ID=60]

读取规则(Read View机制):

  • 事务开始时生成一个Read View,记录当前活跃事务ID列表
  • 读取时,根据版本可见性规则选择合适的版本:
    • 如果记录的TRX_ID小于当前事务ID → 可见
    • 如果记录的TRX_ID在活跃事务列表中 → 不可见,沿版本链向前找
    • 如果记录的TRX_ID大于当前事务ID → 不可见

优势:

  • 读不加锁,写不阻塞读,大幅提升并发性能
  • 实现可重复读隔离级别,无需加锁
  • 支持快照读,适合OLAP场景

3. WAL (Write-Ahead Logging)

WAL流程:
1. 修改数据前,先将变更写入Redo Log
2. Redo Log写入磁盘(fsync)
3. 在内存中修改数据页(Buffer Pool)
4. 数据页择机刷盘(异步)

优势:
- 顺序写日志比随机写数据页快得多
- 即使数据页未刷盘,系统崩溃后也能通过日志恢复
- 支持组提交(Group Commit),提升吞吐量

2.2 分布式事务核心理论

2.2.1 CAP定理

定理内容:一个分布式系统无法同时满足以下三个保证:

  • C (Consistency): 一致性 - 所有节点在同一时刻看到相同的数据
  • A (Availability): 可用性 - 每个请求都能得到响应(成功或失败)
  • P (Partition Tolerance): 分区容错性 - 系统在网络分区情况下仍能继续工作

关键理解:

  • 网络分区是客观存在的(网络故障、延迟等),P是必须满足的
  • 因此实际选择是在C和A之间权衡:
    • CP系统:牺牲可用性保证一致性(如Zookeeper、HBase)
    • AP系统:牺牲强一致性保证可用性(如Cassandra、DynamoDB)

现实意义:

  • 传统ACID事务是CP系统,在分区时会拒绝服务
  • 互联网系统通常选择AP,通过最终一致性补偿
2.2.2 BASE理论

BASE是对CAP理论的延伸,是AP系统的理论基础:

  • BA (Basically Available): 基本可用

    • 允许损失部分可用性(如响应时间增加、功能降级)
    • 但核心功能始终可用
  • S (Soft State): 软状态

    • 系统中的数据可以存在中间状态
    • 允许不同节点间数据同步存在延迟
  • E (Eventually Consistent): 最终一致性

    • 系统保证在没有新更新的情况下,最终所有副本会一致
    • 不保证强一致性,但保证最终收敛

BASE vs ACID对比:

维度 ACID BASE
一致性 强一致性 最终一致性
隔离性 完全隔离 部分隔离
可用性 可能牺牲可用性 高可用性优先
适用场景 金融交易、账务系统 社交网络、内容平台
性能 较低(锁竞争) 较高(无强一致性约束)
2.2.3 一致性模型谱系

分布式系统的一致性是一个连续谱,从强到弱:

强一致性 ←────────────────────────────→ 弱一致性
    ↓                                     ↓
线性一致性 → 顺序一致性 → 因果一致性 → 最终一致性
(Linearizability)(Sequential)(Causal)(Eventual)

1. 线性一致性 (Linearizability)

  • 定义:所有操作看起来像在全局时钟下原子执行
  • 特点:最强的一致性保证,等同于单机系统
  • 代价:性能开销大,通常需要Paxos/Raft协议
  • 应用:配置中心(Zookeeper)、分布式锁

2. 顺序一致性 (Sequential Consistency)

  • 定义:所有节点看到的操作顺序一致,但可能与实际时间顺序不同
  • 特点:比线性一致性弱,但实现成本更低
  • 应用:某些数据库系统的默认隔离级别

3. 因果一致性 (Causal Consistency)

  • 定义:有因果关系的操作必须按顺序观察,无因果关系的可并发
  • 示例:用户发帖后立即能看到自己的帖子,但其他用户可能稍后看到
  • 应用:社交网络、协同编辑

4. 最终一致性 (Eventual Consistency)

  • 定义:如果没有新的更新,所有副本最终会收敛到一致状态
  • 特点:最弱但性能最好,允许短暂的不一致
  • 应用:DNS、CDN、电商商品库存

2.3 分布式事务关键算法

2.3.1 两阶段提交 (2PC)

协议流程:

阶段一:准备阶段(Prepare Phase)
协调者                    参与者A              参与者B
   |                         |                   |
   |------- Prepare -------->|                   |
   |------- Prepare ----------------------->     |
   |                         |                   |
   |<------ Yes/No ------    |                   |
   |<------ Yes/No ----------------------        |

阶段二:提交阶段(Commit Phase)
   |                         |                   |
   |------ Commit/Abort ---->|                   |
   |------ Commit/Abort ----------------------->|
   |                         |                   |
   |<------ ACK ----------   |                   |
   |<------ ACK ----------------------------    |

详细步骤:

准备阶段:

  1. 协调者向所有参与者发送Prepare请求
  2. 参与者执行事务操作(但不提交),将Undo/Redo信息写入日志
  3. 参与者返回Yes(准备成功)或No(准备失败)

提交阶段(基于准备阶段结果):

  • 全部返回Yes: 协调者发送Commit指令,参与者提交事务
  • 任一返回No: 协调者发送Abort指令,参与者回滚事务

优点:

  • 原理简单,易于理解和实现
  • 保证强一致性

缺点:

  1. 同步阻塞:参与者在等待协调者指令期间锁定资源,性能差
  2. 单点故障:协调者宕机导致所有参与者阻塞
  3. 数据不一致风险:如果部分参与者在提交阶段网络故障,可能部分提交部分未提交
  4. 不确定性窗口:协调者发送Commit后宕机,参与者不知道是否应该提交

实际应用限制:

  • 仅适用于低并发、对一致性要求极高的场景(如银行核心系统)
  • 互联网系统基本不使用2PC(性能无法接受)
2.3.2 三阶段提交 (3PC)

3PC是2PC的改进版,引入超时机制和预提交阶段,缓解2PC的阻塞问题。

协议流程:

阶段一:CanCommit - 询问是否可以提交
阶段二:PreCommit - 预提交(但仍可中止)
阶段三:DoCommit - 正式提交

改进点:

  1. 引入超时机制:参与者和协调者都有超时,避免无限期阻塞
  2. 预提交阶段:给参与者更多准备时间,减少最终提交失败概率
  3. 参与者自主决策:超时后参与者可自行提交(假设协调者已发送Commit)

局限性:

  • 仍然存在网络分区下的数据不一致风险
  • 实现复杂度高,性能提升有限
  • 工业界应用极少
2.3.3 TCC (Try-Confirm-Cancel)

TCC是一种补偿型事务方案,将业务逻辑分为三个阶段:

三个阶段:

Try阶段:    尝试执行,预留资源
Confirm阶段: 确认执行,使用预留资源
Cancel阶段:  取消执行,释放预留资源

详细示例(电商下单扣减库存):

Try阶段:

// 库存服务
public boolean tryReduceInventory(String productId, int quantity) {
    // 不直接扣减库存,而是冻结库存
    UPDATE inventory 
    SET available = available - quantity,
        frozen = frozen + quantity
    WHERE product_id = productId 
      AND available >= quantity;
    
    return affected_rows > 0;
}

Confirm阶段:

public void confirmReduceInventory(String transactionId) {
    // 将冻结库存转为已扣减
    UPDATE inventory 
    SET frozen = frozen - quantity
    WHERE transaction_id = transactionId;
}

Cancel阶段:

public void cancelReduceInventory(String transactionId) {
    // 释放冻结库存
    UPDATE inventory 
    SET available = available + quantity,
        frozen = frozen - quantity
    WHERE transaction_id = transactionId;
}

完整流程:

订单服务          库存服务          积分服务
   |                 |                 |
   |---- Try ------->|                 |
   |                 |---- 冻结库存 ---|
   |---- Try ------------------------>|
   |                 |                 |---- 冻结积分 ---|
   |                 |                 |
   [所有Try成功]
   |                 |                 |
   |--- Confirm ---->|                 |
   |                 |---- 扣减库存 ---|
   |--- Confirm -------------------->|
   |                 |                 |---- 扣减积分 ---|

优点:

  • 无需资源锁定,性能优于2PC
  • 业务可控性强,开发者清楚知道每步在做什么
  • 适合长事务场景

缺点:

  • 业务侵入性强,需要开发者实现Try/Confirm/Cancel逻辑
  • 存在空回滚、幂等性等技术挑战
  • 需要事务日志记录,增加存储成本

适用场景:

  • 资金类业务(转账、充值、提现)
  • 库存扣减
  • 积分/优惠券发放
2.3.4 Saga模式

Saga是一种长事务解决方案,将大事务拆分为一系列本地事务,每个本地事务都有对应的补偿事务。

两种实现方式:

1. 协同式Saga (Choreography-based)

每个服务完成本地事务后,发布事件触发下一个服务:

订单服务 → [创建订单] → 发布OrderCreated事件
              ↓
库存服务 → [监听OrderCreated] → 扣减库存 → 发布InventoryReduced事件
              ↓
支付服务 → [监听InventoryReduced] → 扣款 → 发布PaymentCompleted事件

失败补偿流程:

如果支付失败 → 发布PaymentFailed事件
              ↓
库存服务 → [监听PaymentFailed] → 恢复库存 → 发布InventoryRestored事件
              ↓
订单服务 → [监听InventoryRestored] → 取消订单

2. 编排式Saga (Orchestration-based)

中央协调器负责编排各个步骤:

Saga协调器
   |
   |--- 步骤1: 调用订单服务.createOrder()
   |--- 步骤2: 调用库存服务.reduceInventory()
   |--- 步骤3: 调用支付服务.charge()
   |
   [如果步骤3失败]
   |
   |--- 补偿2: 调用库存服务.restoreInventory()
   |--- 补偿1: 调用订单服务.cancelOrder()

优点:

  • 无需2PC协议,性能好
  • 业务流程清晰,易于理解
  • 支持长事务(小时级、天级)

缺点:

  • 缺乏隔离性:事务执行过程中,中间状态对外可见
  • 补偿逻辑复杂:需要仔细设计补偿操作
  • 无法保证原子性:补偿过程中可能再次失败

适用场景:

  • 跨多个服务的长流程业务(订单处理、审批流程)
  • 对一致性要求不是特别严格的场景
  • 微服务架构下的常规选择
2.3.5 本地消息表 + 最终一致性

核心思想:将分布式事务转化为本地事务 + 消息队列的可靠投递。

实现流程:

步骤1: 服务A在本地事务中
       - 执行业务操作
       - 向本地消息表插入待发送消息
       - 提交本地事务

步骤2: 定时任务扫描本地消息表
       - 将未发送的消息发送到消息队列
       - 标记消息为已发送

步骤3: 服务B消费消息
       - 执行业务操作
       - 返回ACK

步骤4: 服务A收到ACK后
       - 将消息标记为已确认

详细示例(用户注册送积分):

// 用户服务
@Transactional
public void registerUser(User user) {
    // 1. 插入用户记录
    userRepository.save(user);
    
    // 2. 插入本地消息表
    LocalMessage message = new LocalMessage();
    message.setTopic("user.registered");
    message.setPayload(JSON.toJSONString(user));
    message.setStatus(MessageStatus.PENDING);
    messageRepository.save(message);
    
    // 3. 事务提交(原子性保证)
}

// 定时任务
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
    List<LocalMessage> messages = messageRepository
        .findByStatus(MessageStatus.PENDING);
    
    for (LocalMessage msg : messages) {
        try {
            // 发送到消息队列
            mqProducer.send(msg.getTopic(), msg.getPayload());
            
            // 更新状态
            msg.setStatus(MessageStatus.SENT);
            messageRepository.save(msg);
        } catch (Exception e) {
            // 记录错误,下次重试
            log.error("Send message failed", e);
        }
    }
}

// 积分服务(消费者)
@RabbitListener(queues = "user.registered")
public void handleUserRegistered(String payload) {
    User user = JSON.parseObject(payload, User.class);
    
    // 幂等性检查(防止重复消费)
    if (pointsRepository.existsByUserId(user.getId())) {
        return;
    }
    
    // 发放积分
    Points points = new Points();
    points.setUserId(user.getId());
    points.setAmount(100);
    pointsRepository.save(points);
}

优点:

  • 实现简单,无需复杂的分布式事务框架
  • 性能好,本地事务开销小
  • 可靠性高,消息不会丢失

缺点:

  • 需要额外的消息表和定时任务
  • 存在一定延迟(非实时一致性)
  • 消费者需要实现幂等性

适用场景:

  • 对实时性要求不高的业务(积分发放、通知发送)
  • 异步处理场景
  • 高并发写入场景

2.4 分布式事务方案对比表

方案 一致性 性能 复杂度 业务侵入性 适用场景
2PC 强一致性 差(阻塞) 中等 低(框架处理) 金融核心系统、对一致性要求极高的场景
3PC 强一致性 中等 几乎不用(理论意义大于实际)
TCC 最终一致性 高(需实现3个接口) 资金类、库存类、需要业务补偿的场景
Saga 最终一致性 中等 中等(需设计补偿) 长流程业务、微服务编排
本地消息表 最终一致性 很好 异步处理、高并发写入
事务消息 最终一致性 很好 与消息队列深度集成的场景

第三部分:实际应用场景

3.1 本地事务典型场景

场景1:银行转账

业务需求:

  • 从账户A转账100元到账户B
  • 保证要么全部成功,要么全部失败
  • 任何时刻账户余额不能为负
  • 转账记录必须准确

实现代码(基于Spring + MyBatis):

@Service
public class TransferService {
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Autowired
    private TransferLogMapper transferLogMapper;
    
    @Transactional(isolation = Isolation.READ_COMMITTED, 
                   rollbackFor = Exception.class)
    public void transfer(String fromAccountId, 
                        String toAccountId, 
                        BigDecimal amount) {
        // 1. 验证参数
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("金额必须大于0");
        }
        
        // 2. 锁定并查询源账户(SELECT FOR UPDATE 加排他锁)
        Account fromAccount = accountMapper
            .selectByIdForUpdate(fromAccountId);
        
        if (fromAccount == null) {
            throw new AccountNotFoundException("源账户不存在");
        }
        
        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException("余额不足");
        }
        
        // 3. 锁定并查询目标账户
        Account toAccount = accountMapper
            .selectByIdForUpdate(toAccountId);
        
        if (toAccount == null) {
            throw new AccountNotFoundException("目标账户不存在");
        }
        
        // 4. 执行转账
        fromAccount.setBalance(
            fromAccount.getBalance().subtract(amount)
        );
        toAccount.setBalance(
            toAccount.getBalance().add(amount)
        );
        
        accountMapper.updateById(fromAccount);
        accountMapper.updateById(toAccount);
        
        // 5. 记录转账日志
        TransferLog log = new TransferLog();
        log.setFromAccountId(fromAccountId);
        log.setToAccountId(toAccountId);
        log.setAmount(amount);
        log.setStatus(TransferStatus.SUCCESS);
        log.setCreateTime(new Date());
        
        transferLogMapper.insert(log);
        
        // 6. 事务提交(方法结束时自动提交)
    }
}

关键技术点:

  • 使用@Transactional注解声明事务边界
  • SELECT FOR UPDATE加排他锁,防止并发修改
  • 设置隔离级别为READ_COMMITTED,平衡性能和一致性
  • 设置rollbackFor = Exception.class,任何异常都回滚

死锁预防:

  • 按账户ID排序后加锁,避免循环等待
// 确保总是先锁ID小的账户
if (fromAccountId.compareTo(toAccountId) > 0) {
    String temp = fromAccountId;
    fromAccountId = toAccountId;
    toAccountId = temp;
    amount = amount.negate(); // 金额取反
}
场景2:电商库存扣减

业务需求:

  • 用户下单时扣减库存
  • 防止超卖(库存不能为负)
  • 高并发情况下保证数据准确性

方案对比:

方案1:悲观锁(FOR UPDATE)

@Transactional
public boolean reduceInventory(String productId, int quantity) {
    // 锁定库存记录
    Inventory inventory = inventoryMapper
        .selectByIdForUpdate(productId);
    
    if (inventory.getStock() < quantity) {
        return false; // 库存不足
    }
    
    inventory.setStock(inventory.getStock() - quantity);
    inventoryMapper.updateById(inventory);
    
    return true;
}
  • 优点:逻辑简单,强一致性
  • 缺点:并发性能差,容易形成锁竞争

方案2:乐观锁(Version字段)

@Transactional
public boolean reduceInventory(String productId, int quantity) {
    // 不加锁查询
    Inventory inventory = inventoryMapper
        .selectById(productId);
    
    if (inventory.getStock() < quantity) {
        return false;
    }
    
    // 带版本号更新
    int affected = inventoryMapper.updateWithVersion(
        productId, 
        quantity, 
        inventory.getVersion()
    );
    
    return affected > 0; // 如果版本号不匹配,返回0
}

// Mapper
UPDATE inventory 
SET stock = stock - #{quantity},
    version = version + 1
WHERE product_id = #{productId}
  AND version = #{version}
  AND stock >= #{quantity}
  • 优点:无锁,并发性能好
  • 缺点:失败需要重试,业务代码需要处理

方案3:Redis预减 + 异步扣减

// 1. 先在Redis中预扣减
public boolean preReduceInventory(String productId, int quantity) {
    String key = "inventory:" + productId;
    Long remaining = redisTemplate.opsForValue()
        .decrement(key, quantity);
    
    if (remaining < 0) {
        // 恢复Redis
        redisTemplate.opsForValue().increment(key, quantity);
        return false;
    }
    
    // 2. 发送MQ消息,异步扣减数据库
    mqProducer.send(new InventoryReduceMessage(
        productId, quantity
    ));
    
    return true;
}

// 3. 消费者处理
@RabbitListener(queues = "inventory.reduce")
public void handleReduce(InventoryReduceMessage msg) {
    inventoryMapper.reduceStock(
        msg.getProductId(), 
        msg.getQuantity()
    );
}
  • 优点:性能极高,可支撑秒杀场景
  • 缺点:Redis与DB数据可能短暂不一致

3.2 分布式事务典型场景

场景1:电商下单流程(Saga模式)

业务流程:

  1. 创建订单(订单服务)
  2. 扣减库存(库存服务)
  3. 扣减余额(账户服务)
  4. 增加积分(积分服务)
  5. 创建物流单(物流服务)

Saga编排实现:

@Service
public class OrderSagaOrchestrator {
    
    public void createOrder(OrderRequest request) {
        SagaContext context = new SagaContext();
        
        try {
            // 步骤1: 创建订单
            String orderId = orderService.createOrder(request);
            context.put("orderId", orderId);
            
            // 步骤2: 扣减库存
            boolean inventoryReduced = inventoryService
                .reduceInventory(request.getProductId(), request.getQuantity());
            
            if (!inventoryReduced) {
                throw new SagaException("库存不足");
            }
            context.put("inventoryReduced", true);
            
            // 步骤3: 扣减余额
            boolean charged = accountService
                .charge(request.getUserId(), request.getAmount());
            
            if (!charged) {
                throw new SagaException("余额不足");
            }
            context.put("charged", true);
            
            // 步骤4: 增加积分
            pointsService.addPoints(
                request.getUserId(), 
                calculatePoints(request.getAmount())
            );
            context.put("pointsAdded", true);
            
            // 步骤5: 创建物流单
            shippingService.createShippingOrder(orderId, request.getAddress());
            
            // 全部成功,标记订单为已支付
            orderService.markAsPaid(orderId);
            
        } catch (Exception e) {
            // 执行补偿
            compensate(context, e);
            throw e;
        }
    }
    
    private void compensate(SagaContext context, Exception cause) {
        log.error("Saga failed, starting compensation", cause);
        
        // 逆序补偿
        if (context.getBoolean("pointsAdded")) {
            try {
                pointsService.deductPoints(
                    context.get("userId"), 
                    context.get("points")
                );
            } catch (Exception e) {
                log.error("Compensate points failed", e);
            }
        }
        
        if (context.getBoolean("charged")) {
            try {
                accountService.refund(
                    context.get("userId"), 
                    context.get("amount")
                );
            } catch (Exception e) {
                log.error("Compensate account failed", e);
            }
        }
        
        if (context.getBoolean("inventoryReduced")) {
            try {
                inventoryService.restoreInventory(
                    context.get("productId"), 
                    context.get("quantity")
                );
            } catch (Exception e) {
                log.error("Compensate inventory failed", e);
            }
        }
        
        // 取消订单
        orderService.cancelOrder(context.get("orderId"));
    }
}

关键设计点:

  1. 幂等性:每个服务的接口必须支持幂等(防止重复补偿)
  2. 补偿顺序:按业务依赖关系逆序补偿
  3. 补偿失败处理:记录日志,人工介入或定时重试
  4. 状态机:订单状态流转要清晰(待支付→已支付→已发货→已完成)
场景2:跨行转账(TCC模式)

业务需求:

  • 从银行A的账户转账到银行B的账户
  • 保证强一致性(不能出现钱凭空消失或增多)
  • 支持长事务(可能需要等待银行间清算)

TCC实现:

// 转账服务
@Service
public class InterBankTransferService {
    
    @Autowired
    private TccTransactionManager tccManager;
    
    public void transfer(String fromBankId, String fromAccountId,
                        String toBankId, String toAccountId,
                        BigDecimal amount) {
        
        TccTransaction tcc = tccManager.begin();
        
        try {
            // Try阶段:冻结资金
            BankAService.tryFreeze(fromAccountId, amount, tcc.getXid());
            BankBService.tryReserve(toAccountId, amount, tcc.getXid());
            
            // 模拟银行间通信和清算
            boolean clearingSuccess = interbankClearingService
                .requestClearing(fromBankId, toBankId, amount);
            
            if (clearingSuccess) {
                // Confirm阶段:确认转账
                tccManager.confirm(tcc);
            } else {
                // Cancel阶段:取消转账
                tccManager.cancel(tcc);
            }
            
        } catch (Exception e) {
            tccManager.cancel(tcc);
            throw e;
        }
    }
}

// 银行A服务 - Try接口
@TccTry
public boolean tryFreeze(String accountId, BigDecimal amount, String xid) {
    Account account = accountRepository.findById(accountId);
    
    if (account.getAvailableBalance().compareTo(amount) < 0) {
        return false;
    }
    
    // 冻结资金
    account.setAvailableBalance(
        account.getAvailableBalance().subtract(amount)
    );
    account.setFrozenBalance(
        account.getFrozenBalance().add(amount)
    );
    
    accountRepository.save(account);
    
    // 记录TCC事务日志
    tccLogRepository.save(new TccLog(
        xid, accountId, amount, TccPhase.TRY
    ));
    
    return true;
}

// 银行A服务 - Confirm接口
@TccConfirm
public void confirmFreeze(String xid) {
    TccLog log = tccLogRepository.findByXid(xid);
    Account account = accountRepository.findById(log.getAccountId());
    
    // 从冻结余额扣除(实际转出)
    account.setFrozenBalance(
        account.getFrozenBalance().subtract(log.getAmount())
    );
    
    accountRepository.save(account);
    
    // 更新日志状态
    log.setPhase(TccPhase.CONFIRMED);
    tccLogRepository.save(log);
}

// 银行A服务 - Cancel接口
@TccCancel
public void cancelFreeze(String xid) {
    TccLog log = tccLogRepository.findByXid(xid);
    
    // 幂等性检查
    if (log.getPhase() == TccPhase.CANCELLED) {
        return;
    }
    
    Account account = accountRepository.findById(log.getAccountId());
    
    // 解冻资金
    account.setAvailableBalance(
        account.getAvailableBalance().add(log.getAmount())
    );
    account.setFrozenBalance(
        account.getFrozenBalance().subtract(log.getAmount())
    );
    
    accountRepository.save(account);
    
    // 更新日志状态
    log.setPhase(TccPhase.CANCELLED);
    tccLogRepository.save(log);
}

TCC技术挑战:

  1. 空回滚问题:
    • 问题:Try阶段因网络超时未执行,但Cancel执行了
    • 解决:Cancel时检查是否存在Try记录,不存在则忽略
@TccCancel
public void cancelFreeze(String xid) {
    TccLog log = tccLogRepository.findByXid(xid);
    
    // 空回滚检查
    if (log == null) {
        log.warn("Try not executed, ignore cancel. xid={}", xid);
        return;
    }
    
    // ...正常取消逻辑
}
  1. 幂等性问题:

    • 问题:网络重试导致Confirm/Cancel被多次调用
    • 解决:记录每个阶段的状态,重复调用直接返回
  2. 悬挂问题:

    • 问题:Cancel先于Try执行(极端网络情况)
    • 解决:Try时检查是否已存在Cancel记录
场景3:分布式定时任务(本地消息表)

业务需求:

  • 每天凌晨生成用户账单
  • 账单生成后发送邮件通知
  • 保证账单和邮件的最终一致性

实现方案:

@Service
public class BillingService {
    
    @Transactional
    public void generateBill(String userId, LocalDate date) {
        // 1. 生成账单
        Bill bill = new Bill();
        bill.setUserId(userId);
        bill.setDate(date);
        bill.setAmount(calculateAmount(userId, date));
        billRepository.save(bill);
        
        // 2. 插入本地消息表
        LocalMessage message = new LocalMessage();
        message.setTopic("bill.generated");
        message.setBusinessId(bill.getId());
        message.setPayload(JSON.toJSONString(bill));
        message.setStatus(MessageStatus.PENDING);
        message.setRetryCount(0);
        message.setMaxRetry(3);
        messageRepository.save(message);
        
        // 3. 事务提交
    }
}

// 定时扫描发送消息
@Component
public class MessageSender {
    
    @Scheduled(fixedDelay = 10000) // 10秒扫描一次
    public void sendPendingMessages() {
        List<LocalMessage> messages = messageRepository
            .findByStatus(MessageStatus.PENDING);
        
        for (LocalMessage msg : messages) {
            try {
                mqProducer.send(msg.getTopic(), msg.getPayload());
                
                msg.setStatus(MessageStatus.SENT);
                msg.setSentTime(new Date());
                messageRepository.save(msg);
                
            } catch (Exception e) {
                msg.setRetryCount(msg.getRetryCount() + 1);
                
                if (msg.getRetryCount() >= msg.getMaxRetry()) {
                    msg.setStatus(MessageStatus.FAILED);
                    // 告警通知人工处理
                    alertService.send("Message send failed: " + msg.getId());
                }
                
                messageRepository.save(msg);
            }
        }
    }
}

// 邮件服务消费消息
@Service
public class EmailService {
    
    @RabbitListener(queues = "bill.generated")
    public void handleBillGenerated(String payload) {
        Bill bill = JSON.parseObject(payload, Bill.class);
        
        // 幂等性检查(防止重复发送邮件)
        if (emailLogRepository.existsByBillId(bill.getId())) {
            log.info("Email already sent for bill: {}", bill.getId());
            return;
        }
        
        // 发送邮件
        emailClient.send(
            bill.getUserEmail(),
            "您的账单已生成",
            renderBillTemplate(bill)
        );
        
        // 记录发送日志
        EmailLog log = new EmailLog();
        log.setBillId(bill.getId());
        log.setSentTime(new Date());
        emailLogRepository.save(log);
    }
}

3.3 真实行业案例

案例1:阿里巴巴双11订单系统

场景复杂度:

  • 峰值:每秒58万笔订单
  • 涉及服务:订单、库存、支付、营销、物流等30+微服务
  • 一致性要求:库存不能超卖,优惠券不能重复使用

技术方案:

  1. 库存扣减:Redis预扣 + 异步DB持久化(TPS优先)
  2. 订单流程:Saga模式编排,每个步骤可异步重试
  3. 支付:TCC模式保证强一致性(资金类必须强一致)
  4. 数据最终一致性:消息队列 + 对账系统(T+1对账)

关键优化:

  • 热点商品库存前置到Redis,减少DB压力
  • 订单创建采用异步化,前端轮询查询结果
  • 补偿任务通过定时任务批量处理,而非实时
案例2:美团外卖配送系统

场景:

  • 用户下单 → 商家接单 → 骑手接单 → 配送 → 完成
  • 每个环节都可能取消,需要回滚

技术方案:

  • 状态机:订单状态精细化管理(15个状态)
  • 补偿策略:
    • 用户取消:退款 + 恢复优惠券 + 释放骑手
    • 商家取消:退款 + 赔付优惠券
    • 配送超时:自动退款 + 补偿

实现要点:

// 状态机定义
enum OrderState {
    CREATED,          // 已创建
    PAID,            // 已支付
    RESTAURANT_ACCEPTED, // 商家已接单
    PREPARING,       // 备餐中
    READY,           // 餐已备好
    RIDER_ASSIGNED,  // 已分配骑手
    RIDER_PICKED,    // 骑手已取餐
    DELIVERING,      // 配送中
    DELIVERED,       // 已送达
    COMPLETED,       // 已完成
    CANCELLED        // 已取消
}

// 状态转移规则
Map<OrderState, Set<OrderState>> allowedTransitions = Map.of(
    CREATED, Set.of(PAID, CANCELLED),
    PAID, Set.of(RESTAURANT_ACCEPTED, CANCELLED),
    RESTAURANT_ACCEPTED, Set.of(PREPARING, CANCELLED),
    // ...
);

// 取消补偿
public void cancelOrder(String orderId, CancelReason reason) {
    Order order = orderRepository.findById(orderId);
    
    SagaCompensator compensator = new SagaCompensator();
    
    if (order.getState().ordinal() >= OrderState.PAID.ordinal()) {
        compensator.add(() -> paymentService.refund(orderId));
    }
    
    if (order.getState().ordinal() >= OrderState.RIDER_ASSIGNED.ordinal()) {
        compensator.add(() -> riderService.releaseRider(order.getRiderId()));
    }
    
    compensator.execute();
    
    order.setState(OrderState.CANCELLED);
    orderRepository.save(order);
}

第四部分:技术实现与使用方法

4.1 主流框架实战

4.1.1 Seata分布式事务框架

Seata是阿里开源的分布式事务解决方案,支持AT、TCC、Saga、XA四种模式。

核心组件:

  • TC (Transaction Coordinator): 事务协调器,维护全局和分支事务的状态
  • TM (Transaction Manager): 事务管理器,定义全局事务的范围
  • RM (Resource Manager): 资源管理器,管理分支事务处理的资源

AT模式实战(最常用,自动补偿):

// 1. 引入依赖
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>

// 2. 配置文件
seata:
  application-id: order-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

// 3. 业务代码
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryFeignClient inventoryClient;
    
    @Autowired
    private AccountFeignClient accountClient;
    
    @GlobalTransactional(name = "create-order", 
                        rollbackFor = Exception.class)
    public void createOrder(OrderDTO dto) {
        // 1. 创建订单(本地事务)
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setProductId(dto.getProductId());
        order.setAmount(dto.getAmount());
        orderMapper.insert(order);
        
        // 2. 调用库存服务扣减库存(远程事务)
        inventoryClient.reduce(dto.getProductId(), dto.getQuantity());
        
        // 3. 调用账户服务扣款(远程事务)
        accountClient.deduct(dto.getUserId(), dto.getAmount());
        
        // 如果任何一步失败,Seata会自动回滚所有操作
    }
}

// 库存服务
@Service
public class InventoryService {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    // 无需特殊注解,Seata自动管理
    public void reduce(String productId, int quantity) {
        Inventory inventory = inventoryMapper.selectById(productId);
        
        if (inventory.getStock() < quantity) {
            throw new InsufficientStockException();
        }
        
        inventory.setStock(inventory.getStock() - quantity);
        inventoryMapper.updateById(inventory);
    }
}

AT模式原理:

  1. 一阶段:执行业务SQL,拦截并记录前后镜像,生成Undo Log
  2. 二阶段提交:异步删除Undo Log(提交成功)
  3. 二阶段回滚:根据Undo Log反向补偿,恢复数据

优点:

  • 业务代码无侵入(只需加@GlobalTransactional)
  • 性能好(一阶段直接提交)
  • 自动生成反向SQL

局限性:

  • 仅支持关系型数据库
  • 隔离性较弱(已提交数据对其他事务可见)
4.1.2 RocketMQ事务消息

适用场景:需要保证消息发送和本地事务的原子性。

工作流程:

1. 生产者发送半消息(Half Message)到Broker
2. Broker存储半消息,返回成功
3. 生产者执行本地事务
4. 生产者发送Commit/Rollback到Broker
5. Broker根据结果投递消息或丢弃

如果第4步失败:
6. Broker定时回查生产者的事务状态
7. 生产者检查本地事务,返回Commit/Rollback/Unknown

代码实现:

// 生产者
@Service
public class OrderService {
    
    @Autowired
    private TransactionMQProducer producer;
    
    public void createOrder(OrderDTO dto) {
        Message msg = new Message(
            "order-topic",
            "TagA",
            JSON.toJSONString(dto).getBytes()
        );
        
        // 发送事务消息
        producer.sendMessageInTransaction(msg, dto);
    }
}

// 事务监听器
@Component
public class OrderTransactionListener implements TransactionListener {
    
    @Autowired
    private OrderService orderService;
    
    // 执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(
            Message msg, Object arg) {
        try {
            OrderDTO dto = (OrderDTO) arg;
            
            // 执行本地事务(插入订单)
            orderService.insertOrder(dto);
            
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
    
    // 事务状态回查
    @Override
    public LocalTransactionState checkLocalTransaction(
            MessageExt msg) {
        String orderId = msg.getKeys();
        
        // 查询订单是否存在
        Order order = orderService.getById(orderId);
        
        if (order != null) {
            return LocalTransactionState.COMMIT_MESSAGE;
        } else {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
}

// 消费者
@Service
public class InventoryService {
    
    @RocketMQMessageListener(
        topic = "order-topic",
        consumerGroup = "inventory-group"
    )
    public class OrderListener implements RocketMQListener<String> {
        
        @Override
        public void onMessage(String message) {
            OrderDTO dto = JSON.parseObject(message, OrderDTO.class);
            
            // 扣减库存(幂等性处理)
            reduceInventory(dto.getProductId(), dto.getQuantity());
        }
    }
}

关键点:

  • 本地事务成功 → 消息一定会投递
  • 本地事务失败 → 消息一定不会投递
  • 消费者必须实现幂等性(消息可能重复投递)

4.2 最佳实践

4.2.1 事务拆分原则

DO:

  • ✅ 按业务边界拆分事务,避免跨多个聚合根
  • ✅ 关键操作(扣款、扣库存)使用强一致性事务
  • ✅ 非关键操作(发通知、记日志)使用异步最终一致性
  • ✅ 长事务拆分为多个短事务

DON’T:

  • ❌ 将所有操作放在一个大事务中
  • ❌ 在事务中进行RPC调用(容易超时)
  • ❌ 在事务中进行耗时操作(文件上传、复杂计算)

示例:

// ❌ 错误示例:大事务
@Transactional
public void badExample(OrderDTO dto) {
    // 1. 插入订单
    orderMapper.insert(order);
    
    // 2. RPC调用库存服务(可能超时)
    inventoryService.reduce(dto.getProductId(), dto.getQuantity());
    
    // 3. RPC调用账户服务
    accountService.deduct(dto.getUserId(), dto.getAmount());
    
    // 4. 发送邮件(耗时操作)
    emailService.send(dto.getUserEmail(), "订单已创建");
    
    // 5. 上传文件到OSS
    ossService.upload(orderReceipt);
    
    // 事务时间过长,锁资源时间长,性能差
}

// ✅ 正确示例:拆分事务
public void goodExample(OrderDTO dto) {
    // 1. 本地事务:仅插入订单
    String orderId = createOrderInTransaction(dto);
    
    // 2. 分布式事务:扣库存和扣款
    executeDistributedTransaction(orderId, dto);
    
    // 3. 异步操作:发邮件和上传文件
    asyncExecutor.execute(() -> {
        emailService.send(dto.getUserEmail(), "订单已创建");
        ossService.upload(orderReceipt);
    });
}

@Transactional
private String createOrderInTransaction(OrderDTO dto) {
    Order order = new Order();
    // ...设置属性
    orderMapper.insert(order);
    return order.getId();
}
4.2.2 幂等性设计

为什么需要幂等性:

  • 网络重试导致请求重复
  • MQ消息可能重复投递
  • 用户重复点击提交按钮

实现方案:

方案1:唯一索引

CREATE TABLE `orders` (
  `id` bigint PRIMARY KEY,
  `order_no` varchar(64) UNIQUE KEY, -- 业务单号唯一
  `user_id` bigint,
  `amount` decimal(10,2)
);

-- 插入时如果order_no已存在,会报唯一索引冲突
INSERT INTO orders (id, order_no, user_id, amount)
VALUES (123, 'ORD20241209001', 1001, 99.99);

方案2:Token机制

// 1. 获取Token
@GetMapping("/order/token")
public String getToken() {
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(
        "order:token:" + token, 
        "1", 
        5, 
        TimeUnit.MINUTES
    );
    return token;
}

// 2. 提交时验证Token
@PostMapping("/order/create")
public void createOrder(@RequestHeader("X-Token") String token,
                       @RequestBody OrderDTO dto) {
    // 原子性删除Token(仅第一次成功)
    Boolean deleted = redisTemplate.delete("order:token:" + token);
    
    if (!deleted) {
        throw new DuplicateRequestException("请勿重复提交");
    }
    
    // 执行业务逻辑
    orderService.create(dto);
}

方案3:状态机

// 订单状态只能单向流转
public void payOrder(String orderId) {
    Order order = orderMapper.selectById(orderId);
    
    // 幂等性检查
    if (order.getStatus() != OrderStatus.CREATED) {
        log.warn("Order already processed: {}", orderId);
        return; // 已处理过,直接返回
    }
    
    // 执行支付
    paymentService.pay(order);
    
    // 更新状态
    order.setStatus(OrderStatus.PAID);
    orderMapper.updateById(order);
}

方案4:去重表

// 专门的去重表
CREATE TABLE `idempotent_log` (
  `business_id` varchar(64) PRIMARY KEY,
  `business_type` varchar(32),
  `create_time` datetime
);

// 使用
@Transactional
public void processOrder(String orderId) {
    // 尝试插入去重记录
    try {
        IdempotentLog log = new IdempotentLog();
        log.setBusinessId(orderId);
        log.setBusinessType("ORDER_PAY");
        idempotentMapper.insert(log);
    } catch (DuplicateKeyException e) {
        // 已处理过
        return;
    }
    
    // 执行业务逻辑
    doPayOrder(orderId);
}
4.2.3 超时与重试策略

超时设置:

// 1. 数据库事务超时
@Transactional(timeout = 30) // 30秒
public void createOrder(OrderDTO dto) {
    // ...
}

// 2. HTTP调用超时
@FeignClient(name = "inventory-service")
public interface InventoryClient {
    
    @GetMapping("/inventory/reduce")
    @RequestLine(
        connectTimeout = 1000,  // 连接超时1秒
        readTimeout = 3000      // 读取超时3秒
    )
    void reduce(@RequestParam String productId, 
                @RequestParam int quantity);
}

// 3. RPC调用超时
@DubboReference(timeout = 5000) // 5秒
private AccountService accountService;

// 4. MQ消费超时
@RabbitListener(
    queues = "order.created",
    containerFactory = "rabbitListenerContainerFactory"
)
public void handleOrder(OrderMessage msg) {
    // 设置消息确认超时时间
}

重试策略:

// Spring Retry
@Service
public class OrderService {
    
    @Retryable(
        value = {TimeoutException.class, RemoteException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 1000, multiplier = 2)
    )
    public void callRemoteService(String orderId) {
        // 远程调用
        // 第1次失败:等待1秒后重试
        // 第2次失败:等待2秒后重试
        // 第3次失败:抛出异常
    }
    
    @Recover
    public void recover(Exception e, String orderId) {
        // 重试全部失败后的降级处理
        log.error("All retries failed for order: {}", orderId, e);
        
        // 记录到失败队列,人工处理
        failedOrderQueue.add(orderId);
    }
}

// 指数退避
public class ExponentialBackoff {
    
    public <T> T executeWithRetry(
            Supplier<T> operation,
            int maxRetries) {
        
        int attempt = 0;
        
        while (attempt < maxRetries) {
            try {
                return operation.get();
            } catch (Exception e) {
                attempt++;
                
                if (attempt >= maxRetries) {
                    throw e;
                }
                
                // 指数退避:2^attempt * 100ms
                long delay = (long) Math.pow(2, attempt) * 100;
                Thread.sleep(delay);
            }
        }
        
        throw new RuntimeException("Max retries exceeded");
    }
}

4.3 常见陷阱与误区

陷阱1:事务传播行为误用
// ❌ 错误示例
@Service
public class OrderService {
    
    @Autowired
    private OrderService self; // 注入自己
    
    @Transactional
    public void createOrder(OrderDTO dto) {
        // ...创建订单
        
        // 想要新开事务记录日志,但实际不会生效!
        this.saveLog(dto); // 直接调用,不经过Spring代理
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(OrderDTO dto) {
        // 期望独立事务,实际与createOrder在同一事务
    }
}

// ✅ 正确示例
@Service
public class OrderService {
    
    @Autowired
    private LogService logService; // 注入其他Service
    
    @Transactional
    public void createOrder(OrderDTO dto) {
        // ...创建订单
        
        // 通过其他Bean调用,经过Spring代理,生效!
        logService.saveLog(dto);
    }
}

@Service
public class LogService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveLog(OrderDTO dto) {
        // 新事务,即使createOrder回滚,日志也会保存
    }
}
陷阱2:长事务导致死锁
// ❌ 错误示例
@Transactional
public void badLongTransaction() {
    // 1. 锁定账户A
    Account accountA = accountMapper.selectByIdForUpdate("A");
    
    // 2. 进行复杂计算(耗时30秒)
    BigDecimal result = complexCalculation();
    
    // 3. 锁定账户B
    Account accountB = accountMapper.selectByIdForUpdate("B");
    
    // 如果其他事务先锁B再锁A,就会死锁!
}

// ✅ 正确示例
public void goodShortTransaction() {
    // 1. 先做计算(不在事务中)
    BigDecimal result = complexCalculation();
    
    // 2. 短事务:按固定顺序加锁
    executeInTransaction(() -> {
        List<String> ids = Arrays.asList("A", "B");
        Collections.sort(ids); // 排序保证加锁顺序
        
        Account accountA = accountMapper.selectByIdForUpdate(ids.get(0));
        Account accountB = accountMapper.selectByIdForUpdate(ids.get(1));
        
        // 快速执行业务逻辑
        accountA.setBalance(result);
        accountMapper.update(accountA);
    });
}
陷阱3:忽略补偿失败
// ❌ 错误示例
private void compensate(SagaContext context) {
    if (context.get("inventoryReduced")) {
        inventoryService.restore(context.get("productId"));
        // 如果restore失败怎么办?数据就不一致了!
    }
}

// ✅ 正确示例
private void compensate(SagaContext context) {
    if (context.get("inventoryReduced")) {
        try {
            inventoryService.restore(context.get("productId"));
        } catch (Exception e) {
            // 记录补偿失败
            compensationLogRepository.save(new CompensationLog(
                context.getTransactionId(),
                "RESTORE_INVENTORY",
                CompensationStatus.FAILED,
                e.getMessage()
            ));
            
            // 发送告警
            alertService.send(
                "Compensation failed: " + context.getTransactionId()
            );
            
            // 可选:加入重试队列
            retryQueue.add(new RetryTask(
                "restore_inventory",
                context.get("productId"),
                3 // 最大重试次数
            ));
        }
    }
}

// 定时重试失败的补偿
@Scheduled(fixedDelay = 60000)
public void retryFailedCompensations() {
    List<CompensationLog> failed = compensationLogRepository
        .findByStatus(CompensationStatus.FAILED);
    
    for (CompensationLog log : failed) {
        if (log.getRetryCount() < 3) {
            try {
                retryCompensation(log);
                log.setStatus(CompensationStatus.SUCCESS);
            } catch (Exception e) {
                log.setRetryCount(log.getRetryCount() + 1);
            }
            compensationLogRepository.save(log);
        } else {
            // 超过重试次数,人工介入
            manualInterventionQueue.add(log);
        }
    }
}

第五部分:未来发展趋势

5.1 云原生时代的事务处理

Serverless与事务:

  • 挑战:函数计算无状态,难以维护事务上下文
  • 解决方案:
    • 使用云原生事务协调服务(AWS Step Functions、阿里云Saga)
    • 基于事件溯源(Event Sourcing)的最终一致性
    • FaaS编排框架(如Temporal)

Service Mesh中的事务:

应用层:专注业务逻辑
    ↓
Sidecar:处理重试、超时、熔断
    ↓
Control Plane:统一事务协调

5.2 NewSQL数据库的分布式事务

代表产品:

  • TiDB:基于Percolator算法的分布式事务
  • CockroachDB:基于Raft的强一致性事务
  • OceanBase:支持跨地域的分布式事务

技术特点:

  • 在数据库层面原生支持分布式事务
  • 性能接近NoSQL,一致性保证强于传统方案
  • 对应用透明,业务代码无需感知分布式

示例(TiDB):

-- 普通SQL,但底层是分布式事务
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;

-- TiDB自动处理:
-- 1. 分布式快照隔离
-- 2. 两阶段提交
-- 3. 跨Region的事务协调

5.3 区块链与分布式账本

技术结合点:

  • 区块链天然支持分布式一致性(共识算法)
  • 智能合约可实现自动化补偿
  • 不可篡改特性天然支持审计

应用场景:

  • 跨机构的资金清算
  • 供应链金融
  • 数字资产交易

5.4 AI辅助的事务优化

智能诊断:

  • 基于机器学习的死锁预测
  • 自动识别事务热点和性能瓶颈
  • 智能推荐事务拆分策略

异常检测:

  • 实时监控事务执行时间,识别异常慢事务
  • 预测事务失败概率,提前告警
  • 自动化根因分析

示例:

AI Agent观察到:
- 订单创建事务平均耗时从100ms增加到5s
- 分析发现:库存服务响应慢
- 自动建议:启用库存预扣 + 异步扣减方案

第六部分:事务方案决策树

是否跨多个服务/数据库?
│
├─ 否 → 使用本地事务
│       │
│       ├─ 高并发场景 → MVCC + 乐观锁
│       └─ 强一致性场景 → 悲观锁(FOR UPDATE)
│
└─ 是 → 分布式事务
        │
        ├─ 能容忍最终一致性?
        │   │
        │   ├─ 是 → 
        │   │    │
        │   │    ├─ 异步处理 → 本地消息表/事务消息
        │   │    ├─ 长流程 → Saga
        │   │    └─ 复杂补偿 → TCC
        │   │
        │   └─ 否 → 
        │        │
        │        ├─ 性能要求高 → TCC
        │        ├─ 简单场景 → 2PC(不推荐)
        │        └─ 使用NewSQL数据库 → 数据库原生分布式事务
        │
        └─ 是否有业务补偿能力?
             │
             ├─ 是 → Saga/TCC
             └─ 否 → 重新设计业务流程

选择建议表:

场景 推荐方案 理由
电商下单 Saga 流程长,允许最终一致性
支付转账 TCC 资金类必须强一致性
积分发放 本地消息表 异步,性能要求高
秒杀扣库存 Redis预扣+异步DB 极高并发
跨行转账 TCC 长事务,强一致性
用户注册 本地事务 单服务,无需分布式

第七部分:常见误区总结

误区1:“分布式事务一定要用2PC”

  • ❌ 错误:2PC性能差,现代系统几乎不用
  • ✅ 正确:优先考虑Saga、TCC、最终一致性

误区2:“最终一致性就是不一致”

  • ❌ 错误:最终一致性也是一致性,只是有时间窗口
  • ✅ 正确:通过补偿机制保证最终收敛到一致状态

误区3:“分布式事务可以完全替代本地事务”

  • ❌ 错误:能用本地事务就不用分布式事务
  • ✅ 正确:分布式事务有性能开销,能合并服务就合并

误区4:“Saga模式不需要考虑隔离性”

  • ❌ 错误:Saga缺乏隔离性,中间状态对外可见
  • ✅ 正确:需要通过业务设计规避(如订单状态控制)

误区5:“使用框架就万事大吉”

  • ❌ 错误:框架只是工具,业务逻辑要自己设计
  • ✅ 正确:幂等性、补偿逻辑、异常处理都需要仔细设计

第八部分:学习路径建议

8.1 基础阶段

  1. 理论学习(1-2周)

    • 深入理解ACID特性
    • 学习事务隔离级别
    • 掌握CAP/BASE理论
  2. 本地事务实践(2-3周)

    • MySQL事务特性实验
    • 锁机制与死锁排查
    • MVCC原理验证

推荐资源:

  • 《高性能MySQL》第1、7章
  • MySQL官方文档:InnoDB事务模型
  • 数据库内核月报:MVCC实现

8.2 进阶阶段

  1. 分布式事务理论(2周)

    • 深入学习2PC、3PC、Paxos、Raft
    • 理解各种一致性模型
    • 研究经典论文(Percolator、Spanner)
  2. 框架实战(4-6周)

    • Seata:AT、TCC、Saga模式实践
    • RocketMQ事务消息
    • 自己实现简单的Saga框架

推荐资源:

  • 《数据密集型应用系统设计》第7-9章
  • Seata官方文档
  • Google Percolator论文

8.3 高级阶段

  1. 性能优化(4周)

    • 事务性能调优
    • 死锁诊断与预防
    • 大规模分布式事务架构设计
  2. 故障排查(持续学习)

    • 生产环境问题案例分析
    • 监控体系建设
    • 应急预案制定

推荐资源:

  • 阿里、美团技术博客(分布式事务实践)
  • AWS/阿里云分布式事务服务文档
  • DDIA作者Martin Kleppmann的博客

8.4 专家阶段

  1. 深入内核

    • 阅读数据库源码(MySQL InnoDB、TiDB)
    • 研究分布式共识算法实现
    • 参与开源项目贡献
  2. 架构设计

    • 为复杂业务场景设计分布式事务方案
    • 制定企业级事务治理规范
    • 推动技术演进和落地

推荐资源:

  • TiDB源码分析
  • MIT 6.824分布式系统课程
  • VLDB、SIGMOD顶会论文

总结

事务处理是软件工程中的核心技术之一,从单机ACID到分布式最终一致性,是技术演进与业务需求平衡的结果。

核心要点回顾:

  1. 本质理解:事务是为了保证数据一致性,ACID是手段,一致性是目标
  2. 方案选择:没有银弹,根据业务特点选择合适方案
  3. 工程实践:幂等性、超时、重试、补偿缺一不可
  4. 持续学习:分布式事务是活跃的研究领域,新技术不断涌现
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐