欢迎光临
我们一直在努力

分布式事务全方案对比与总结(seata AT)

分布式事务全方案对比与总结(seata AT)

在微服务架构中,分布式事务是绕不开的难题。本文从原理、核心组件、执行流程、核心机制、优缺点、适用场景六个维度,对主流的分布式事务方案进行全面对比和总结,帮助你根据业务场景做出合理的技术选型。

Seata AT 模式完全指南

一、原理

Seata AT 模式是基于二阶段提交(2PC) 的改良实现,核心思想是:利用本地事务 + 自动生成反向SQL(回滚日志),对业务代码零侵入。

核心三组件

组件角色职责
TC(Transaction Coordinator) 事务协调器(服务端) 维护全局事务状态,控制全局提交或回滚
TM(Transaction Manager) 事务管理器(客户端) 标记全局事务边界(@GlobalTransactional)
RM(Resource Manager) 资源管理器(客户端) 管理本地数据源,执行SQL并记录undo_log

执行流程

  • 一阶段(Prepare):执行业务SQL,同时生成Before Image和After Image存入undo_log,与业务数据在同一个本地事务中提交,随后释放本地数据库锁。
  • 二阶段(Commit/Rollback):
    • 提交:TC异步通知各RM删除undo_log,释放全局锁。
    • 回滚:TC通知各RM根据undo_log中的Before Image生成反向SQL恢复数据。

优缺点

优点
  • 对业务代码"零侵入":只需在入口方法加@GlobalTransactional注解,框架自动完成SQL拦截、镜像生成、回滚日志等全流程。
  • 保证强一致性(ACID级别):通过lock_table实现写隔离,确保在全局事务提交前,其他分布式事务无法修改同一行数据。
  • 回滚机制自动化:基于undo_log自动生成反向SQL,无需人工编写补偿代码。
  • 社区活跃,生态完善:Apache顶级项目,对Spring Cloud Alibaba、Dubbo等主流框架有原生支持。
  • 缺点
  • 性能瓶颈:全局锁争用:本质是分布式悲观锁,高并发热点数据场景下TPS显著下降。
  • 数据库与SQL的硬性约束:
    • 业务表必须有单列主键(联合主键不支持)。
    • 不支持INSERT ON DUPLICATE KEY UPDATE、REPLACE INTO、多表关联更新等复杂SQL。
    • 仅支持具备本地事务能力的关系型数据库(MySQL、Oracle、PostgreSQL等)。
  • undo_log存储成本:存储整行数据的完整JSON快照,批量更新时日志膨胀严重,回滚耗时增加。
  • 脏数据处理不完美:若回滚前数据被非Seata管理的应用修改,会导致DataValidationFailedException,放弃自动回滚,需人工介入。
  • 无法协调非数据库资源:对于短信发送、第三方API调用等无法生成undo_log的操作,一旦全局回滚,这些外部操作无法自动回滚,需配合TCC或本地消息表做补偿。
  • 额外运维成本:需独立部署和维护Seata-Server(TC)集群,并定期清理lock_table和global_table。
  • 选型原则:金融、账务等低并发、强一致性场景优先选AT;电商秒杀等高并发、允许短暂不一致场景应选用TCC或本地消息表方案。

    二、使用前准备

    1. 服务端(Seata-Server)

    下载与部署

    从官方GitHub下载最新版本的 seata-server。解压后进入 /bin 目录,执行启动脚本(Windows为 .bat,Linux/Mac为 .sh)。

    配置存储模式 (store.mode)

    Seata支持 file、db、redis 三种模式。

    • file:单机模式,配置简单,无需额外依赖,适合开发或学习,不适合生产环境。
    • db:高可用模式,生产环境推荐,事务信息存储在数据库中,支持集群部署。需在 conf/file.conf 中修改 store.mode="db" 并配置数据库连接。
    • redis:高性能模式,适合对性能要求极高的场景。
    初始化数据库表(db模式)

    如果使用 db 模式,必须在 Seata-Server 使用的数据库中创建三张核心表:

    表名作用
    global_table 存储全局事务状态
    branch_table 存储分支事务明细
    lock_table 存储全局锁信息(AT模式专用)

    SQL脚本可在Seata源码的 script/server/db/ 目录下找到。

    配置注册中心 (registry)

    为了让微服务能发现Seata-Server,需配置注册中心(如Nacos、Eureka、Zookeeper)。在 conf/registry.conf 中配置 registry.type 和相关参数。

    registry.type=nacos
    registry.nacos.server-addr=127.0.0.1:8848
    registry.nacos.namespace=public
    registry.nacos.group=SEATA_GROUP

    2. 客户端(微服务)

    每个参与分布式事务的微服务,其业务数据库中都必须创建一张 undo_log 表。

    创建 undo_log 表

    用于AT模式记录回滚日志。脚本可在Seata源码的 script/client/at/db/ 目录下找到。

    CREATE TABLE `undo_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id` bigint(20) NOT NULL,
    `xid` varchar(100) NOT NULL,
    `context` varchar(128) NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int(11) NOT NULL,
    `log_created` datetime NOT NULL,
    `log_modified` datetime NOT NULL,
    `ext` varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    引入依赖

    推荐使用 seata-spring-boot-starter,它会自动配置核心组件。

    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
    </dependency>

    配置核心参数(application.yml)

    seata:
    enabled: true
    application-id: ${spring.application.name}
    # 事务分组名称,可自定义,需与Server端映射关系对应
    tx-service-group: my_tx_group
    service:
    # 事务分组到TC集群的映射
    vgroup-mapping:
    my_tx_group: default # default为Seata-Server端配置的集群名称
    registry:
    # 注册中心类型,推荐Nacos
    type: nacos
    nacos:
    server-addr: 127.0.0.1:8848
    namespace: ""
    group: "SEATA_GROUP"

    数据源代理(核心!)

    AT模式通过代理数据源来拦截SQL,生成回滚日志(undo_log)。

    方案一(推荐):自动代理

    seata-spring-boot-starter 默认开启自动数据源代理(seata.enable-auto-data-source-proxy=true),无需手动编写任何代码,框架会自动将容器中的 DataSource 包装为 DataSourceProxy。

    启动日志中出现以下内容即表示自动代理生效:
    [INFO] [SeataAutoDataSourceProxyCreator] – Auto proxy of [dataSource]

    方案二:手动代理

    以下场景需要手动配置数据源代理:

  • 项目中使用了多数据源(如 dynamic-datasource),自动代理可能只代理了默认数据源。
  • 与分库分表中间件(如 ShardingSphere)共用,自动代理可能导致"代理的代理"引发类型转换异常。
  • 项目中有自定义 DataSource 初始化逻辑(如加密、动态路由),需精确控制代理时机。
  • 手动代理配置示例:

    @Configuration
    public class DataSourceConfig {
    @Bean
    @Primary
    public DataSource dataSource(DruidDataSource druidDataSource) {
    // 手动将原生数据源包装为 DataSourceProxy
    return new DataSourceProxy(druidDataSource);
    }
    }

    ⚠️ 常见坑点:如果忘记代理数据源,Seata无法拦截SQL,事务将无法回滚!

    其他可选配置

    seata:
    client:
    rm:
    lock:
    retry-interval: 10 # 全局锁重试间隔(ms),默认10
    retry-times: 30 # 全局锁重试次数,默认30
    async-commit-buffer-limit: 10000 # 异步提交队列大小
    config:
    type: nacos # 配置中心类型,可选nacos/apollo/etcd等

    三、流程图

    库存数据库 (RM-库存)库存服务 (RM-库存)订单数据库 (RM-订单)TC (Seata服务端)TM (订单服务-入口)库存数据库 (RM-库存)库存服务 (RM-库存)订单数据库 (RM-订单)TC (Seata服务端)TM (订单服务-入口)#mermaid-svg-AP9F57B5ycETrOjK{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AP9F57B5ycETrOjK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AP9F57B5ycETrOjK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AP9F57B5ycETrOjK .error-icon{fill:#552222;}#mermaid-svg-AP9F57B5ycETrOjK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AP9F57B5ycETrOjK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AP9F57B5ycETrOjK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AP9F57B5ycETrOjK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AP9F57B5ycETrOjK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AP9F57B5ycETrOjK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AP9F57B5ycETrOjK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AP9F57B5ycETrOjK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AP9F57B5ycETrOjK .marker.cross{stroke:#333333;}#mermaid-svg-AP9F57B5ycETrOjK svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AP9F57B5ycETrOjK p{margin:0;}#mermaid-svg-AP9F57B5ycETrOjK .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AP9F57B5ycETrOjK text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-AP9F57B5ycETrOjK .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-AP9F57B5ycETrOjK .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-AP9F57B5ycETrOjK .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-AP9F57B5ycETrOjK .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-AP9F57B5ycETrOjK #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-AP9F57B5ycETrOjK .sequenceNumber{fill:white;}#mermaid-svg-AP9F57B5ycETrOjK #sequencenumber{fill:#333;}#mermaid-svg-AP9F57B5ycETrOjK #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-AP9F57B5ycETrOjK .messageText{fill:#333;stroke:none;}#mermaid-svg-AP9F57B5ycETrOjK .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AP9F57B5ycETrOjK .labelText,#mermaid-svg-AP9F57B5ycETrOjK .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-AP9F57B5ycETrOjK .loopText,#mermaid-svg-AP9F57B5ycETrOjK .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-AP9F57B5ycETrOjK .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-AP9F57B5ycETrOjK .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-AP9F57B5ycETrOjK .noteText,#mermaid-svg-AP9F57B5ycETrOjK .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-AP9F57B5ycETrOjK .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AP9F57B5ycETrOjK .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AP9F57B5ycETrOjK .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AP9F57B5ycETrOjK .actorPopupMenu{position:absolute;}#mermaid-svg-AP9F57B5ycETrOjK .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-AP9F57B5ycETrOjK .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AP9F57B5ycETrOjK .actor-man circle,#mermaid-svg-AP9F57B5ycETrOjK line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-AP9F57B5ycETrOjK :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}阶段一(Prepare):执行本地事务 + 记录undo_log服务端拦截器取出 XID,绑定到本线程 ThreadLocal若锁冲突(另一事务正持有),此处阻塞重试直至超时par[订单服务执行本地事务 (TM侧)][库存服务执行本地事务 (RM侧)]阶段二(Commit):全局提交(正常情况)par[分支提交 (异步并行)][分支提交 (异步并行)]1. 发起全局事务 (Begin)2. 生成并返回 XID3. 将 XID 绑定到当前线程 ThreadLocal4. RPC 调用扣库存 (请求头携带 XID)5. 执行本地业务 SQL (insert order)6. 向 TC 注册分支事务 (Branch Register)7. 申请全局锁 (锁定 order_id)8. 生成 undo_log + 业务SQL(在同一本地事务中提交)本地事务提交成功9. 执行本地业务 SQL (update stock)10. 向 TC 注册分支事务 (Branch Register)11. 申请全局锁 (锁定 product_id)12. 生成 undo_log + 业务SQL(在同一本地事务中提交)本地事务提交成功13. RPC 调用返回成功14. 发起全局提交 (Global Commit)15. 下发分支提交指令 (异步)16. 下发分支提交指令 (异步)17. 删除本地 undo_log18. 释放全局锁19. 删除本地 undo_log20. 释放全局锁21. 全局事务完成

    四、核心机制伪代码

    以下代码为 Seata 内部框架逻辑,无需业务开发人员手动实现,仅供理解原理。

    1. TM 端:全局事务拦截器

    @Around("@annotation(GlobalTransactional)")
    public Object invoke(MethodInvocation inv) throws Throwable {
    // 1. 向 TC 请求开启全局事务,获取 XID
    String xid = TransactionCoordinator.beginGlobalTransaction("order-service");

    // 2. 将 XID 绑定到当前线程的 ThreadLocal(RootContext)
    RootContext.bind(xid);

    try {
    // 3. 执行目标方法(业务逻辑,内含 RPC 调用)
    Object result = inv.proceed();

    // 4. 业务执行成功,TM 向 TC 发起全局提交
    TransactionCoordinator.commitGlobalTransaction(xid);
    return result;

    } catch (Throwable e) {
    // 5. 业务异常,TM 向 TC 发起全局回滚
    TransactionCoordinator.rollbackGlobalTransaction(xid);
    throw e;
    } finally {
    // 6. 清理线程上下文
    RootContext.unbind();
    }
    }
    }

    2. RPC 客户端拦截器:透传 XID

    // 模拟 Feign 的 RequestInterceptor 或 Dubbo 的 Filter
    public class RpcClientInterceptor {

    public void beforeSendRequest(RpcRequest request) {
    // 1. 从当前线程 ThreadLocal 中获取 XID
    String xid = RootContext.getXID();

    // 2. 如果 XID 不为空,塞入协议的附加参数中
    if (xid != null) {
    request.addHeader("TX_XID", xid); // Feign 场景
    // 或 request.setAttachment("TX_XID", xid); // Dubbo 场景
    }
    }
    }

    3. RPC 服务端拦截器:接收 XID

    // 模拟 Dubbo Filter 或 Spring MVC Interceptor
    public class RpcServerInterceptor {

    public void beforeHandle(RpcRequest request) {
    // 1. 从请求头/附件中提取 XID
    String xid = request.getHeader("TX_XID");

    // 2. 如果不为空,绑定到当前服务端线程的 ThreadLocal
    if (xid != null) {
    RootContext.bind(xid);
    }
    }
    }

    4. RM 端:数据源代理(核心 SQL 执行器)

    // 模拟 DataSourceProxy 中的 Connection 代理
    public class ConnectionProxy {

    public void executeUpdate(String sql, Object[] params) throws SQLException {
    // 1. 检查当前线程是否有 XID
    String xid = RootContext.getXID();
    if (xid == null) {
    // 没有全局事务,直接执行普通 SQL
    realConnection.execute(sql, params);
    return;
    }

    // 2. 解析 SQL,获取表名和条件(Seata 内置 SQLParser)
    SqlMeta meta = SqlParser.parse(sql);

    // 3. 查询 Before Image(修改前的数据快照)
    String selectSql = "SELECT * FROM " + meta.tableName + " WHERE " + meta.conditions;
    List<Map<String, Object>> beforeImage = realConnection.query(selectSql);

    // 4. 向 TC 注册分支事务 + 申请全局锁(锁冲突时阻塞重试)
    Long branchId = TransactionCoordinator.registerBranch(
    xid, meta.resourceId, meta.tableName, meta.primaryKeyValues
    );
    RootContext.bindBranchId(branchId);

    // 5. 执行业务 SQL(实际的 update/insert/delete)
    realConnection.execute(sql, params);

    // 6. 查询 After Image(修改后的数据快照)
    List<Map<String, Object>> afterImage = realConnection.query(selectSql);

    // 7. 构建并插入 undo_log(与业务 SQL 在同一本地事务中)
    UndoLog log = new UndoLog();
    log.setXid(xid);
    log.setBranchId(branchId);
    log.setBeforeImage(beforeImage);
    log.setAfterImage(afterImage);
    realConnection.execute("INSERT INTO undo_log (xid, branch_id, rollback_info) VALUES (?, ?, ?)", log);

    // 8. 提交本地事务(业务数据和 undo_log 同时落盘)
    realConnection.commit();
    // 注意:此时全局锁并未释放,需等待二阶段结束
    }
    }

    5. TC 端:协调器核心逻辑

    // 模拟 TC 服务端逻辑
    public class TransactionCoordinator {

    // —– 一阶段调用 —–
    public static String beginGlobalTransaction(String appId) {
    String xid = generateXid(getLocalIp(), port, incrementId());
    insertGlobalTable(xid, appId, status=1);
    return xid;
    }

    // 注册分支 + 申请全局锁(线程安全,存在并发争抢)
    public synchronized static Long registerBranch(String xid, String resourceId, String table, String pk) {
    // 检查 lock_table 是否存在冲突
    if (lockTableExist(resourceId, table, pk) && !isSameXid(xid)) {
    throw new LockConflictException("Global lock conflict, retrying…");
    }
    insertLockTable(xid, resourceId, table, pk);
    Long branchId = generateBranchId();
    insertBranchTable(branchId, xid, resourceId, status=0);
    return branchId;
    }

    // —– 二阶段调用 —–
    public static void commitGlobalTransaction(String xid) {
    updateGlobalStatus(xid, status=3);
    notifyAllRMsToCommit(xid); // 异步通知各RM删除undo_log
    }

    public static void rollbackGlobalTransaction(String xid) {
    updateGlobalStatus(xid, status=4);
    notifyAllRMsToRollback(xid); // 通知各RM执行回滚
    }
    }

    6. RM 端:二阶段回滚处理器

    public class RollbackHandler {

    public void handleRollback(String xid, Long branchId) {
    // 1. 查询本地 undo_log
    UndoLog undoLog = queryUndoLog(xid, branchId);

    // 2. 脏数据校验:当前数据是否等于 After Image
    Map<String, Object> currentData = queryCurrentData(undoLog.getTableName(), undoLog.getPrimaryKey());
    if (!currentData.equals(undoLog.getAfterImage())) {
    // 数据被其他事务修改,无法自动回滚
    throw new DirtyDataException("Data changed, cannot rollback automatically!");
    }

    // 3. 根据 Before Image 生成反向 SQL
    String rollbackSql = generateReverseSql(undoLog);
    realConnection.execute(rollbackSql);

    // 4. 删除 undo_log 并释放全局锁
    realConnection.execute("DELETE FROM undo_log WHERE xid = ? AND branch_id = ?", xid, branchId);
    TransactionCoordinator.unlockGlobalLock(xid, branchId);
    }
    }

    五、常见问题与解决方案

    1. 全局锁争用(性能杀手)

    • 问题:AT通过lock_table实现写隔离(分布式悲观锁),高并发热点数据下RT飙升、线程池耗尽。
    • 解决:调小client.rm.lock.retry-times实现快速失败;核心高并发场景(秒杀)不要用AT,改用TCC或Redis预扣库存。

    2. 数据库与SQL的硬性约束

    • 问题:业务表必须有单列主键;不支持INSERT ON DUPLICATE KEY UPDATE、多表关联更新等复杂SQL。
    • 解决:Code Review阶段加入门禁,禁止复杂DML,拆分为多条简单SQL。

    3. undo_log 膨胀与全量回滚风险

    • 问题:rollback_info存储完整JSON快照,大批量更新(如update set status=1 where create_time < '2020-01-01')会导致日志膨胀,回滚缓慢甚至失败。
    • 解决:AT事务内避免大批量更新,改为循环分批执行;监控表大小,定时清理超期日志。

    4. 脏数据风险(回滚校验失败)

    • 问题:二阶段回滚前,数据被非Seata应用或手动SQL修改,导致currentData ≠ afterImage,Seata抛出DataValidationFailedException并放弃自动回滚。
    • 解决:严格禁止绕过Seata对业务数据的直接修改,所有修改必须走带@GlobalTransactional的微服务入口。

    5. 代码层面的"隐式"陷阱

    • 陷阱一(吞异常):@GlobalTransactional方法内try-catch后未throw,TM误以为成功发起提交 → 数据不一致。解决:捕获后必须throw。
    • 陷阱二(异步丢XID):事务方法内new Thread或@Async,子线程RootContext拿不到XID,RM退化成本地事务。解决:手动传递XID(RootContext.bind),或使用TransmittableThreadLocal。
    • 陷阱三(外部调用无法回滚):AT无法回滚短信、第三方API等非数据库操作。解决:将外部调用移出@GlobalTransactional(事务后置),或封装为TCC模式,或使用本地消息表。

    6. TC(服务端)的高可用与存储风险

    • 问题:TC默认file模式,重启后事务状态丢失,锁永久不释放;集群脑裂或网络抖动时提交请求丢失。
    • 解决:生产环境强制使用db或redis模式;TC部署集群(至少2+1),使用Nacos做注册中心并配置重试机制;编写定时任务清理lock_table和global_table中的死记录。

    7. 事务分组的作用

    事务分组是客户端查找TC的路由标识,核心价值:

    • 多环境隔离:同一套代码通过环境变量注入不同分组名连接不同环境的TC。
    • 业务隔离:核心业务与非核心业务分到不同分组,避免相互影响。
    • 容灾与灰度:配合注册中心动态切换主备集群,或实现事务层面的金丝雀发布。

    六、面试常见问题

    Q1:Seata AT 模式的优缺点分别是什么?

    答:

    • 优点:代码零侵入(只需@GlobalTransactional)、强一致性(ACID级别)、回滚自动化、社区活跃。
    • 缺点:全局锁导致高并发性能瓶颈、SQL和表结构有严格限制、undo_log存储成本高、脏数据处理不完美、无法协调非数据库资源(短信/API调用)、需独立部署维护TC。

    Q2:AT 模式的全局锁是怎么实现的?为什么能保证写隔离?

    答:全局锁由 TC 维护,存储在 lock_table 中,是一种基于唯一键约束实现的逻辑分布式锁(而非依赖数据库行锁)。RM 在一阶段本地事务提交之前,必须向 TC 申请全局锁(锁粒度为 resource_id + table_name + pk)。仅当插入 lock_table 成功(唯一键无冲突)时,本地事务才允许提交;若插入失败,则进入阻塞等待 + 循环重试(可配置),超时后本地事务回滚并释放数据库本地锁。其他全局事务修改同一行时,会因唯一键冲突而被阻塞,从而保证写隔离。

    ⚠️ 风险与约束:若存在未接入 Seata 的本地事务直接修改同一行数据,因该事务无需申请全局锁,会导致:

    全局提交时:本地事务覆盖全局事务的修改,数据“静默丢失”,Seata 无感知;

    全局回滚时:undo_log 中的前镜像与数据库实际数据不一致,回滚失败,需人工介入修复。

    ✅ 防范措施:

    架构层面:所有修改同一张表的应用,必须统一接入 Seata 事务组;

    代码层面:在全局事务中对关键数据使用 SELECT … FOR UPDATE,强制申请全局锁,阻塞未接入事务的干扰。

    Q3:Seata AT 和 TCC 的区别?如何选型?

    答:

    • AT:零侵入、开发快、强一致性,但性能受全局锁影响。适合低并发、强一致性场景(如金融账务)。
    • TCC:侵入性强(需手写try/confirm/cancel)、开发复杂,但性能高(无全局锁)。适合高并发、允许最终一致性场景(如电商秒杀)。

    Q4:@GlobalTransactional 方法内调用了另一个带 @GlobalTransactional 的方法,会怎样?

    答:Seata默认传播行为是REQUIRED,内层方法会加入外层事务,不会开启新事务。如果内层异常,外层也会回滚。AT模式下强烈不建议嵌套使用@GlobalTransactional,容易造成死锁或状态混乱。

    Q5:XID 是怎么传递到下游服务的?丢失了怎么办?

    答:Seata通过RPC拦截器自动透传——客户端将XID塞入请求头(Feign)或Attachment(Dubbo),服务端拦截器取出并绑定到当前线程的RootContext(ThreadLocal)。
    丢失原因:异步线程(new Thread/@Async)导致ThreadLocal不传递。
    解决方案:手动调用RootContext.bind(xid)传递,或使用TransmittableThreadLocal。

    Q6:undo_log 表是做什么的?什么时候写入和删除?

    答:

    • 写入:一阶段执行业务SQL后、本地事务提交之前,RM生成Before/After Image并存入undo_log,与业务数据在同一本地事务提交。
    • 删除:二阶段收到TC提交指令时,RM异步删除undo_log;收到回滚指令时,利用undo_log生成反向SQL恢复数据,然后删除日志。

    Q7:AT 模式下,一阶段本地事务提交后,数据对其他事务可见吗?为什么?

    答:可见,但无法修改。因为一阶段已提交了本地事务,其他事务的SELECT能读到数据(因此AT的隔离级别被称为"读未提交")。但如果其他全局事务要修改同一行,会因为lock_table全局锁冲突而被阻塞,直到原事务二阶段完成。这就是"读写不阻塞,写写阻塞"的特性。

    Q8:如果 TC 宕机了,正在运行的全局事务会怎样?

    答:取决于TC的存储模式:

    • file模式:TC重启后内存状态丢失,事务永久悬挂,锁永不释放 → 需人工干预。
    • db/redis模式:TC重启后可恢复global_table和branch_table中的事务状态,继续完成二阶段。因此生产环境必须使用db或redis模式。

    Q9:如何在 AT 事务中处理不可回滚的外部资源(如发送短信)?

    答:AT无法回滚外部API调用。三种解决策略:

  • 后置提交:将外部调用移出@GlobalTransactional,放到事务提交后执行。
  • TCC封装:将外部服务封装为TCC模式,提供补偿(Cancel)接口。
  • 本地消息表:事务内只插入待发送记录,定时任务异步发送并重试,保证最终一致性。
  • 七、总结

    Seata AT 模式通过自动生成反向SQL + 全局锁,在零业务代码侵入的前提下实现了分布式强一致性事务。它的核心设计哲学是用性能换开发效率,将复杂的2PC逻辑下沉到中间件层。

    维度评估
    开发效率 ⭐⭐⭐⭐⭐(仅需一个注解)
    数据一致性 ⭐⭐⭐⭐⭐(ACID级别)
    性能表现 ⭐⭐⭐(全局锁是瓶颈)
    运维复杂度 ⭐⭐⭐(需独立部署TC)
    适用场景 金融账务、订单核心链路(低并发、强一致性)

    核心选型原则:

    • 金融、账务、核心订单 → 优先AT,开发快,框架兜底回滚。
    • 电商秒杀、营销抢购 → 不用AT,改用TCC或本地消息表,通过最终一致性保证高吞吐。
    • 任何场景 → 遵循 “数据库操作在事务内,网络IO在事务外” 的铁律,避免外部资源无法回滚的陷阱。

    Seata AT 是分布式事务领域的"懒人神器",但只有理解其底层机制和局限性,才能在高并发生产环境中正确使用它。这也是区分架构师与普通开发者的关键分水岭。

    赞(0)
    未经允许不得转载:171主机测评 » 分布式事务全方案对比与总结(seata AT)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址