Skip to content

AT模式深度剖析与隔离性问题

AT模式的实现原理

AT模式(Auto-Commit Transaction)是Seata中使用最广泛的事务模式,它通过数据源代理的方式实现了对业务代码的零侵入。理解AT模式的底层实现机制,对于正确使用它、排查问题至关重要。

数据源代理机制

AT模式的核心是数据源代理。Seata在应用的DataSource之上包装了一层代理层,将原本的JDBC DataSource转换为Seata DataSourceProxy。这样就可以在代理层拦截所有的SQL执行,控制事务的提交和回滚。

mermaid
graph TB
    A[业务代码] --> B[Seata DataSourceProxy]
    B --> C[SQL解析器]
    B --> D[UndoLog生成器]
    B --> E[全局锁管理]
    B --> F[原始DataSource]
    F --> G[数据库]
    
    C -.-> H[解析SQL类型<br/>表名/字段/条件]
    D -.-> I[生成before/after镜像]
    E -.-> J[申请/释放全局锁]
    
    style A fill:#a8e6cf,stroke:#333,stroke-width:2px,rx:10,ry:10
    style B fill:#ffd3b6,stroke:#333,stroke-width:2px,rx:10,ry:10
    style F fill:#dcedc1,stroke:#333,stroke-width:2px,rx:10,ry:10
    style G fill:#dcedc1,stroke:#333,stroke-width:2px,rx:10,ry:10

两阶段提交详解

AT模式将事务分为两个阶段执行,每个阶段都有明确的职责和执行步骤。

第一阶段:业务SQL执行与日志记录

mermaid
sequenceDiagram
    participant App as 应用程序
    participant Proxy as DataSourceProxy
    participant TC as 事务协调器
    participant DB as 数据库

    App->>Proxy: 执行UPDATE语句
    Proxy->>Proxy: 1.解析SQL
    Proxy->>DB: 2.查询变更前数据(before image)
    DB-->>Proxy: 返回当前数据
    Proxy->>DB: 3.执行业务SQL
    DB-->>Proxy: 执行成功
    Proxy->>DB: 4.查询变更后数据(after image)
    DB-->>Proxy: 返回新数据
    Proxy->>DB: 5.插入undo log
    Proxy->>TC: 6.注册分支并申请全局锁
    TC-->>Proxy: 注册成功
    Proxy->>DB: 7.提交本地事务
    Proxy-->>App: 返回执行结果

详细步骤说明:

步骤1:SQL解析
代理层拦截到SQL后,首先进行解析,提取出SQL类型(INSERT/UPDATE/DELETE)、涉及的表名、修改的字段以及WHERE条件等关键信息。

步骤2:生成前置镜像(before image)
根据SQL解析结果,先执行一次查询,获取即将被修改的数据的当前状态,作为before image。这个镜像数据是后续回滚的依据。

例如,对于SQL:

sql
UPDATE product SET stock = stock - 5 WHERE product_id = 100;

会先执行:

sql
SELECT product_id, stock FROM product WHERE product_id = 100;
-- 假设查询结果:product_id=100, stock=50

步骤3:执行业务SQL
执行实际的业务SQL,对数据库进行修改。

步骤4:生成后置镜像(after image)
业务SQL执行完成后,再次查询被修改的数据,获取变更后的状态,作为after image。

sql
SELECT product_id, stock FROM product WHERE product_id = 100;
-- 执行后结果:product_id=100, stock=45

步骤5:插入undo log
将before image、after image以及业务SQL的相关信息组装成一条回滚日志,插入到undo_log表中。

undo log的数据结构示例:

json
{
  "branchId": 987654321,
  "xid": "192.168.1.100:8091:2024120200001",
  "undoItems": [{
    "sqlType": "UPDATE",
    "tableName": "product",
    "beforeImage": {
      "rows": [{
        "fields": [{
          "name": "product_id",
          "type": 4,
          "value": 100
        }, {
          "name": "stock",
          "type": 4,
          "value": 50
        }]
      }]
    },
    "afterImage": {
      "rows": [{
        "fields": [{
          "name": "product_id",
          "type": 4,
          "value": 100
        }, {
          "name": "stock",
          "type": 4,
          "value": 45
        }]
      }]
    }
  }]
}

步骤6:注册分支事务并申请全局锁
向TC注册分支事务,并申请本次修改的记录的全局锁。全局锁是Seata用来保证多个全局事务之间隔离性的重要机制。

步骤7:提交本地事务
将业务数据的变更和undo log的插入一起提交到数据库。这里依赖数据库本地事务的ACID特性,保证业务操作和回滚日志的原子性。

关键点: 第一阶段结束后,本地事务已经提交,数据库锁已经释放,但全局锁仍然被持有。

第二阶段:提交或回滚

第二阶段根据全局事务的决议,执行不同的操作。

场景一:全局提交

如果所有分支事务的第一阶段都执行成功,TC会通知所有RM提交全局事务。

mermaid
sequenceDiagram
    participant TC as 事务协调器
    participant RM as 资源管理器
    participant DB as 数据库

    TC->>RM: 通知提交全局事务
    RM->>RM: 释放全局锁
    RM->>DB: 异步删除undo log
    RM-->>TC: 提交成功

由于第一阶段已经提交了本地事务,所以第二阶段的提交非常轻量:

  1. 释放全局锁
  2. 异步删除undo log(无需立即删除,可批量处理)

这种设计使得AT模式在正常情况下性能非常高。

场景二:全局回滚

如果任一分支事务失败,TC会协调所有RM进行回滚。

mermaid
sequenceDiagram
    participant TC as 事务协调器
    participant RM as 资源管理器
    participant DB as 数据库

    TC->>RM: 通知回滚全局事务
    RM->>DB: 1.查询XID和BranchID对应的undo log
    DB-->>RM: 返回undo log
    RM->>RM: 2.校验数据一致性
    RM->>DB: 3.生成并执行反向SQL
    RM->>DB: 4.删除undo log
    RM->>DB: 5.提交本地事务
    RM->>RM: 6.释放全局锁
    RM-->>TC: 回滚成功

回滚过程详解:

步骤1:查找undo log
根据XID和Branch ID从undo_log表中查询对应的回滚日志。

步骤2:数据一致性校验
将当前数据库中的数据与undo log中的after image进行比对:

  • 如果一致,说明数据未被其他事务修改,可以安全回滚,执行步骤3
  • 如果不一致,再比对before image:
    • 若与before image一致,说明事务未提交或已回滚,无需处理
    • 若都不一致,说明出现了脏写,需要人工介入处理

步骤3:生成反向SQL并执行
根据before image生成反向SQL,恢复数据到事务执行前的状态。

例如,对于之前的UPDATE操作,会生成:

sql
UPDATE product SET stock = 50 WHERE product_id = 100;

步骤4-6:清理和释放资源
删除undo log,提交本地事务,释放全局锁。

AT模式 vs XA模式

AT模式和XA模式都基于两阶段提交思想,但实现机制截然不同,导致它们在性能和一致性方面有明显差异。

核心区别对比

维度AT模式XA模式
一阶段处理直接提交本地事务,释放数据库锁执行但不提交,持有数据库锁
二阶段处理提交:异步删除日志;回滚:基于undo log补偿提交:通知数据库提交;回滚:通知数据库回滚
资源锁定时间仅一阶段期间跨越两个阶段
一致性最终一致性强一致性
性能

执行流程对比

mermaid
graph TB
    subgraph XA模式
    B1[一阶段开始] --> B2[执行SQL]
    B2 --> B3[准备提交<br/>持有数据库锁]
    B3 --> B4{全局决议}
    B4 -->|提交| B5[正式提交<br/>释放锁]
    B4 -->|回滚| B6[数据库回滚<br/>释放锁]
    end
    
    style B3 fill:#ffd3b6,stroke:#333,stroke-width:2px,rx:10,ry:10
    style B5 fill:#dcedc1,stroke:#333,stroke-width:2px,rx:10,ry:10
    style B6 fill:#ffaaa5,stroke:#333,stroke-width:2px,rx:10,ry:10
mermaid
graph TB
    subgraph XA模式
    B1[一阶段开始] --> B2[执行SQL]
    B2 --> B3[准备提交<br/>持有数据库锁]
    B3 --> B4{全局决议}
    B4 -->|提交| B5[正式提交<br/>释放锁]
    B4 -->|回滚| B6[数据库回滚<br/>释放锁]
    end
    
    style B3 fill:#ffd3b6,stroke:#333,stroke-width:2px,rx:10,ry:10
    style B5 fill:#dcedc1,stroke:#333,stroke-width:2px,rx:10,ry:10
    style B6 fill:#ffaaa5,stroke:#333,stroke-width:2px,rx:10,ry:10

性能差异分析

AT模式的性能优势:

  1. 数据库锁快速释放:第一阶段提交后立即释放行锁,其他事务可以继续访问这些数据
  2. 二阶段异步化:提交场景下的日志清理可以异步批量处理
  3. 仅全局锁等待:只需等待Seata的全局锁,不影响数据库并发能力

XA模式的性能劣势:

  1. 长时间锁定资源:从一阶段到二阶段完成,数据库资源一直被锁定
  2. 阻塞其他事务:其他事务无法访问被锁定的数据,并发能力下降
  3. 数据库负载高:大量事务等待锁释放,数据库连接池可能耗尽

一致性差异分析

XA模式的强一致性:

XA模式在一阶段只进行准备而不提交,所有参与者都处于"准备就绪"状态。只有二阶段收到提交指令后,才真正提交数据。这确保了:

  • 事务未完成前,其他事务看不到中间状态
  • 全局事务具有完整的ACID特性

AT模式的最终一致性:

AT模式在一阶段就提交了本地事务,这意味着:

  • 数据变更立即对其他事务可见
  • 如果后续发生回滚,会存在一个短暂的不一致窗口
  • 属于最终一致性,而非强一致性

AT模式的脏读问题

问题场景分析

AT模式的一阶段会提交本地事务,这带来了一个特殊的隔离性问题:全局事务级别的脏读

注意,这里的脏读与传统数据库的脏读不同:

  • 传统脏读:读取到其他本地事务未提交的数据
  • AT模式脏读:读取到全局事务中某个分支事务已提交但可能被回滚的数据

具体案例

假设有一个电商下单流程,包含三个步骤:

java
@Service
public class OrderService {
    
    @Autowired
    private InventoryClient inventoryClient;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private PaymentClient paymentClient;
    
    @GlobalTransactional
    public void placeOrder(OrderDTO orderDTO) {
        // 步骤1: 扣减库存
        inventoryClient.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
        
        // 步骤2: 创建订单
        orderRepository.createOrder(orderDTO);
        
        // 步骤3: 扣款(假设这里失败)
        paymentClient.deductBalance(orderDTO.getUserId(), orderDTO.getAmount());
    }
}

时间线分析:

mermaid
sequenceDiagram
    participant OrderSvc as 订单服务
    participant InvSvc as 库存服务
    participant QuerySvc as 查询服务
    participant PaySvc as 支付服务
    participant TC as 事务协调器

    OrderSvc->>InvSvc: T1: 扣减库存(商品A,数量10)
    InvSvc->>InvSvc: 执行SQL: stock=90
    InvSvc->>InvSvc: 提交本地事务✓
    
    Note over QuerySvc: T2: 此时查询库存
    QuerySvc->>InvSvc: 查询商品A库存
    InvSvc-->>QuerySvc: 返回:90件
    
    OrderSvc->>PaySvc: T3: 扣款
    PaySvc-->>OrderSvc: 余额不足,失败✗
    
    OrderSvc->>TC: T4: 全局事务回滚
    TC->>InvSvc: 回滚库存
    InvSvc->>InvSvc: 恢复: stock=100
    
    Note over QuerySvc: T5: 查询服务发现<br/>读到了脏数据(90)

在上述流程中:

  • T1时刻:库存服务扣减库存成功,本地事务提交,库存变为90
  • T2时刻:另一个查询服务查询库存,读到了90这个值
  • T3时刻:支付服务扣款失败
  • T4时刻:全局事务回滚,库存恢复为100
  • T5时刻:查询服务发现自己在T2读到的90是"脏数据"

为什么会出现脏读

AT模式的脏读是其设计机制决定的:

  1. 性能优先的设计:为了避免长时间锁定数据库资源,一阶段直接提交本地事务
  2. 全局事务的两阶段特性:一阶段执行成功后,全局事务尚未最终确定,仍可能回滚
  3. 数据可见性:本地事务一旦提交,数据就对其他事务可见,无法阻止读取

如何应对脏读问题

方案一:使用全局锁(SELECT FOR UPDATE)

Seata提供了@GlobalLock注解和SELECT FOR UPDATE语法,可以在查询时申请全局锁,避免读到未提交的全局事务数据。

java
@GlobalLock
@Transactional
public Product queryProductStock(Long productId) {
    // 使用SELECT FOR UPDATE申请全局锁
    return productMapper.selectForUpdate(productId);
}

这种方式会等待全局事务完成后再读取数据,但会降低并发性能。

方案二:业务层面补偿

在业务设计上接受短暂的不一致,通过最终一致性保证数据正确。例如:

  • 库存查询允许一定的误差
  • 使用异步对账机制修正数据

方案三:选择其他事务模式

如果业务对一致性要求极高,不能容忍任何中间状态:

  • 使用XA模式保证强一致性(牺牲性能)
  • 使用TCC模式通过资源预留避免脏读(需要改造代码)

应该如何选择

场景推荐方案理由
库存查询展示接受脏读对用户影响小,性能优先
订单确认页面使用全局锁需要准确数据,可接受轻微延迟
资金账户查询使用XA模式或TCC模式资金类业务不允许任何不一致

小结

AT模式通过数据源代理和undo log机制实现了对业务代码的零侵入,在大多数场景下提供了优秀的性能表现。相比XA模式,AT模式牺牲了强一致性换取了更高的性能,这是一种符合互联网业务特点的权衡。

理解AT模式的脏读问题及其根源,能够帮助我们在实际应用中做出正确的技术选型。对于大多数业务场景,AT模式的最终一致性是可以接受的;而对于资金类等强一致性要求的场景,应选择XA模式或TCC模式。

更新: 2025-12-04 17:42:16
原文: https://www.yuque.com/u22210564/zoxfmt/doc-19-seata-03-at

Java 后端面试知识库