微服务架构下,原本在单机数据库中由本地事务保证的数据一致性,被拆分到多个独立服务和独立数据库中。跨服务的数据操作如果缺乏事务保障,一旦中间环节失败就会出现数据不一致。Seata作为阿里开源的分布式事务解决方案,其AT(Automatic Transaction)模式以低侵入性著称,只需在方法上添加@GlobalTransactional注解即可实现跨服务事务。本文深入AT模式原理并给出完整的Spring Cloud微服务接入实战。
Seata AT模式两阶段提交原理解析
AT模式的核心思路是”自动拦截SQL,生成回滚日志”。与XA协议的两阶段提交不同,AT模式在一阶段直接提交本地事务,释放数据库连接资源,通过undo_log表记录变更前的数据快照。二阶段如果全局事务提交,异步删除undo_log;如果回滚,根据undo_log反向补偿。
一阶段流程:
1. TM(Transaction Manager)向TC(Transaction Coordinator)注册全局事务,获取XID
2. XID通过RPC调用链路传播到下游RM(Resource Manager)
3. 每个RM拦截业务SQL,解析SQL语义,查询变更前数据快照(before image)和变更后数据快照(after image)
4. 将快照存入undo_log表
5. 执行业务SQL并提交本地事务
6. 向TC注册分支事务,上报分支状态
二阶段流程:
全局提交:TC通知各RM异步删除undo_log记录,因为本地事务已经提交,无需补偿。
全局回滚:TC通知各RM根据undo_log的before image反向生成补偿SQL,先校验after image与当前数据是否一致(检测脏写),一致则执行补偿,不一致说明数据已被其他事务修改,需要人工介入。
Seata Server部署与存储模式配置
Seata Server(TC)是事务协调器,独立部署。生产环境推荐使用高可用模式:
# application.yml (Seata Server)
seata:
server:
service:
port: 8091
max-commit-retry-timeout: 10000
max-rollback-retry-timeout: 10000
store:
mode: db
db:
datasource: druid
db-type: mysql
url: jdbc:mysql://mysql.seata.svc:3306/seata?characterEncoding=utf8
user: seata
password: seata_pwd
min-conn: 5
max-conn: 30
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
# Nacos注册中心配置
registry:
type: nacos
nacos:
application: seata-server
server-addr: nacos.seata.svc:8848
namespace: seata
group: SEATA_GROUP
# 配置中心
config:
type: nacos
nacos:
server-addr: nacos.seata.svc:8848
namespace: seata
group: SEATA_GROUP
存储模式选择DB而非file,保证TC宕机后事务状态不丢失。global_table存储全局事务状态,branch_table存储分支事务状态,lock_table存储全局锁。需要在数据库中执行Seata提供的建表SQL:
-- global_table
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint DEFAULT NULL,
`status` tinyint NOT NULL,
`application_id` varchar(64) DEFAULT NULL,
`transaction_service_group` varchar(64) DEFAULT NULL,
`transaction_name` varchar(128) DEFAULT NULL,
`timeout` int DEFAULT NULL,
`begin_time` bigint DEFAULT NULL,
`application_data` varchar(2000) DEFAULT NULL,
`gmt_create` datetime DEFAULT NULL,
`gmt_modified` datetime DEFAULT NULL,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status`,`gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 每个业务库都需要undo_log表
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo log context',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
Spring Boot微服务接入Seata实战
以订单服务和库存服务为例,下单时需要同时扣减库存,保证两个操作的原子性。
Maven依赖:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 数据源代理,AT模式需要拦截SQL -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
订单服务application.yml配置:
seata:
enabled: true
application-id: order-service
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
registry:
type: nacos
nacos:
server-addr: nacos.seata.svc:8848
namespace: seata
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: nacos.seata.svc:8848
namespace: seata
group: SEATA_GROUP
# AT模式数据源代理
data-source-proxy-mode: AT
# 关闭MyBatis的多数据源代理冲突
use-multi-tier-data-source-proxy: false
订单服务业务代码:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryFeignClient inventoryClient;
// 全局事务入口
@GlobalTransactional(timeoutMills = 60000, name = "createOrder")
public Order createOrder(OrderDTO dto) {
// 1. 创建订单(本地事务)
Order order = new Order();
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setAmount(dto.getAmount());
order.setStatus("CREATED");
orderMapper.insert(order);
// 2. 远程调用库存服务扣减库存(分支事务)
// XID通过Feign拦截器自动传播
Result result = inventoryClient.deduct(dto.getProductId(), dto.getQuantity());
if (!result.isSuccess()) {
throw new RuntimeException("库存扣减失败: " + result.getMessage());
}
// 3. 如果后续还有其他远程调用失败,会触发全局回滚
// 订单插入和库存扣减都会自动回滚
return order;
}
}
Feign拦截器配置XID传播:
@Component
public class SeataFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
template.header(RootContext.KEY_XID, xid);
template.header("TX_XID", xid);
}
}
}
// Feign客户端注册拦截器
@FeignClient(name = "inventory-service", configuration = FeignConfig.class)
public interface InventoryFeignClient {
@PostMapping("/api/inventory/deduct")
Result deduct(@RequestParam Long productId, @RequestParam Integer quantity);
}
@Configuration
public class FeignConfig {
@Bean
public SeataFeignInterceptor seataFeignInterceptor() {
return new SeataFeignInterceptor();
}
}
AT模式全局锁与回滚机制详解
AT模式通过全局锁解决脏写问题。一阶段本地事务提交前,RM会向TC申请获取该行数据的全局锁。如果其他全局事务想要修改同一行数据,必须等待全局锁释放。
全局锁的获取在本地事务提交之前完成,流程为:
1. 拦截SQL,获取操作行的主键
2. 向TC申请这些主键的全局锁
3. 获取成功后,将before/after image写入undo_log
4. 提交本地事务
5. 向TC上报分支事务状态
如果第4步提交成功了但第5步上报失败,TC会定时重试与RM通信。如果本地事务在第4步之前失败,TC超时后回滚全局事务,无undo_log需要处理。
回滚时的脏写检测:RM执行补偿前,先用after image与当前数据库中的数据做比对。如果一致,说明中间没有其他事务修改过该数据,可以安全回滚。如果不一致,说明存在脏写,Seata会记录日志并停止自动回滚,转人工处理。
分布式事务性能调优与避坑指南
全局事务超时设置:默认60000ms(60秒)。超时后TC自动发起回滚。如果业务链路较长或包含慢调用,需要适当增大超时时间。但超时过长会导致锁长时间占用,影响并发性能。
// 控制超时的粒度
@GlobalTransactional(timeoutMills = 120000, name = "createOrder")
// rollbackFor属性指定哪些异常触发回滚
@GlobalTransactional(timeoutMills = 60000, rollbackFor = Exception.class, name = "payment")
避免大事务:不要在全局事务中执行耗时操作(如文件上传、外部HTTP调用、批量数据处理)。长事务持锁时间长,极易导致锁等待和超时回滚。将非核心操作移到事务外,通过补偿机制处理。
读写隔离:AT模式默认读未提交(Read Uncommitted)。如果读操作也在全局事务中且需要读到已提交数据,使用@GlobalLock + SELECT FOR UPDATE:
@GlobalTransactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 使用@GlobalLock注解获取全局锁,保证读到最新已提交数据
Account from = accountMapper.selectForUpdate(fromId); // SELECT ... FOR UPDATE
Account to = accountMapper.selectForUpdate(toId);
if (from.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
accountMapper.deduct(fromId, amount);
accountMapper.add(toId, amount);
}
undo_log清理:全局事务提交后,undo_log由Seata异步清理。如果高频事务,undo_log表会快速增长。Seata提供定时清理任务,也可以手动清理已完成的记录:
-- 清理已完成且超过保留期的undo_log
DELETE FROM undo_log
WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY)
AND branch_id IN (
SELECT branch_id FROM seata.branch_table
WHERE status != 0 -- 非进行中的分支
);
Seata AT模式适合强一致性要求较高且可以容忍短时间锁等待的业务场景,如订单创建与库存扣减、转账与账户更新。对于最终一致性可接受的场景(如消息通知、积分发放),更适合使用本地消息表或Seata的TCC/SAGA模式。技术选型时需要根据业务特点权衡,不要所有接口都套用分布式事务。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/seata-fen-bu-shi-shi-wu-at-mo-shi-yuan-li-pou-xi-yu-wei-fu/