分布式事务全方案对比与总结(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恢复数据。
优缺点
优点
缺点
- 业务表必须有单列主键(联合主键不支持)。
- 不支持INSERT ON DUPLICATE KEY UPDATE、REPLACE INTO、多表关联更新等复杂SQL。
- 仅支持具备本地事务能力的关系型数据库(MySQL、Oracle、PostgreSQL等)。
选型原则:金融、账务等低并发、强一致性场景优先选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]
方案二:手动代理
以下场景需要手动配置数据源代理:
手动代理配置示例:
@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调用。三种解决策略:
七、总结
Seata AT 模式通过自动生成反向SQL + 全局锁,在零业务代码侵入的前提下实现了分布式强一致性事务。它的核心设计哲学是用性能换开发效率,将复杂的2PC逻辑下沉到中间件层。
| 开发效率 | ⭐⭐⭐⭐⭐(仅需一个注解) |
| 数据一致性 | ⭐⭐⭐⭐⭐(ACID级别) |
| 性能表现 | ⭐⭐⭐(全局锁是瓶颈) |
| 运维复杂度 | ⭐⭐⭐(需独立部署TC) |
| 适用场景 | 金融账务、订单核心链路(低并发、强一致性) |
核心选型原则:
- 金融、账务、核心订单 → 优先AT,开发快,框架兜底回滚。
- 电商秒杀、营销抢购 → 不用AT,改用TCC或本地消息表,通过最终一致性保证高吞吐。
- 任何场景 → 遵循 “数据库操作在事务内,网络IO在事务外” 的铁律,避免外部资源无法回滚的陷阱。
Seata AT 是分布式事务领域的"懒人神器",但只有理解其底层机制和局限性,才能在高并发生产环境中正确使用它。这也是区分架构师与普通开发者的关键分水岭。




