分布式事务的核心问题与Seata的定位
微服务架构下,一次业务操作可能跨多个服务、操作多个数据库。本地事务无法保证跨服务的数据一致性。Seata是阿里开源的分布式事务解决方案,提供AT、TCC、Saga、XA四种模式。其中AT模式(自动补偿型)使用成本最低——业务代码无需感知分布式事务,Seata自动生成回滚SQL。
AT模式的工作机制:一阶段拦截业务SQL,在本地事务提交前记录修改前后的数据快照(before image / after image)到undo_log表,本地事务直接提交。二阶段如果全局事务成功,异步删除undo_log;如果失败,根据undo_log中的before image自动生成补偿SQL执行回滚。
Seata Server部署与配置
Seata架构中有三个角色:TC(事务协调器)、TM(事务管理器)、RM(资源管理器)。TC是独立部署的Server端,TM和RM是集成在微服务中的SDK。
使用Docker部署TC Server:
docker run -d --name seata-server \
-p 8091:8091 \
-p 7091:7091 \
-e SEATA_IP=192.168.1.100 \
-v /opt/seata/config:/seata-server/resources \
seataio/seata-server:2.0.0
TC的存储模式选择——生产环境使用数据库模式(db模式)而非文件模式(file模式)。application.yml配置:
seata:
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.100:3306/seata?useUnicode=true
user: seata
password: seata_password
# 事务分组配置
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 192.168.1.100:8091
TC数据库需要创建三张表:global_table(全局事务表)、branch_table(分支事务表)、lock_table(全局锁表)。建表SQL在Seata官方仓库的script/server/db/mysql.sql中。
Spring Boot微服务集成Seata AT模式
每个参与分布式事务的微服务都需要集成Seata SDK。Maven依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2023.0.1.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
微服务的application.yml配置:
seata:
enabled: true
application-id: order-service
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
grouplist:
default: 192.168.1.100:8091
# AT模式配置
data-source-proxy-mode: AT
# 关闭数据源自动代理(手动指定需要代理的数据源)
enable-auto-data-source-proxy: true
每个微服务的业务数据库需要创建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,such as serialization',
`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 COMMENT='AT transaction mode undo table';
全局事务的声明与传播
TM通过@GlobalTransactional注解开启全局事务,调用链路上的RM服务自动加入该全局事务:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private StorageFeignClient storageFeignClient;
@GlobalTransactional(timeoutMills = 60000, rollbackFor = Exception.class)
public void createOrder(OrderDTO orderDTO) {
// 1. 创建订单(本地事务,RM自动注册分支事务)
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setCount(orderDTO.getCount());
order.setMoney(orderDTO.getMoney());
order.setStatus(0);
orderMapper.insert(order);
// 2. 远程调用:扣减账户余额(RM自动注册分支事务)
accountFeignClient.decrease(orderDTO.getUserId(), orderDTO.getMoney());
// 3. 远程调用:扣减库存(RM自动注册分支事务)
storageFeignClient.decrease(orderDTO.getProductId(), orderDTO.getCount());
// 4. 修改订单状态
order.setStatus(1);
orderMapper.updateById(order);
// 模拟异常,触发全局回滚
if (orderDTO.getCount() > 100) {
throw new RuntimeException("订单数量超过限制");
}
}
}
关键点:@GlobalTransactional只需要在事务发起方(TM)标注。下游服务(RM)的本地事务由Seata自动协调,不需要任何额外注解。全局事务ID(XID)通过HTTP Header或RPC上下文自动传播。
Feign调用链中的XID传播
Seata通过拦截器自动传播XID,但需要确保拦截器正确注册。OpenFeign场景下需要添加依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
如果XID没有正确传播,下游服务会以普通本地事务执行,全局回滚时无法补偿。验证方式——在下游服务中获取XID:
@RestController
public class AccountController {
@PostMapping("/account/decrease")
public String decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money) {
// 检查XID是否存在
String xid = RootContext.getXID();
if (xid == null) {
log.warn("XID未传播,当前操作不在全局事务中");
} else {
log.info("当前全局事务XID: {}", xid);
}
accountService.decrease(userId, money);
return "success";
}
}
XID传播失败的常见原因:Seata的SeataFeignClient拦截器未生效(检查Spring Cloud Alibaba版本兼容性)、使用了异步线程池调用下游服务(XID存储在ThreadLocal中,跨线程需要手动传播)。
并发更新与全局锁机制
AT模式通过全局锁防止脏写——在本地事务提交前,Seata会向TC申请获取修改行的全局锁。如果另一个全局事务持有该行的锁,当前事务会等待(默认超时10秒)或抛出异常。
全局锁的获取发生在本地事务commit之前——这是AT模式与2PC的关键区别。本地事务一阶段就提交了(释放数据库锁),但全局锁持有到全局事务结束。这样设计是为了减少数据库锁的持有时间,提高并发性能。
脏写场景的回滚问题——如果一个全局事务回滚时,发现after image与数据库当前值不一致(说明被另一个事务修改了),Seata会抛出BranchRollbackFailed_Retriable异常,记录到TC的branch_table中,人工介入处理。
常见排障场景
全局事务无法回滚,分支事务数据未恢复——检查undo_log表是否存在且结构正确。Seata回滚依赖undo_log中的before image,如果undo_log被手动清除或表结构错误,回滚无法执行。查看TC Server日志中BranchRollbackFailed记录。
can not find undo_log异常——undo_log记录在分支事务commit后被删除,但全局回滚时找不到记录。常见于undo_log表与业务表不在同一个数据库。Seata要求undo_log和业务表在同一个datasource中。
死锁与锁超时——LockConflict异常表示全局锁竞争。排查是否存在多个全局事务频繁更新同一行数据。可以通过调整seata.client.rm.lock.retryInterval(锁重试间隔)和retryTimes(重试次数)来缓解。
服务治理层面的超时问题——@GlobalTransactional的timeoutMills默认60秒。如果业务操作链路较长,超时后TC会主动触发回滚,但RPC调用可能仍在执行。确保timeout设置大于所有下游服务调用的最长耗时之和。
AT模式的适用场景与局限
AT模式适用于:同构数据库(都是MySQL或都是PostgreSQL)、单表或简单JOIN操作、对并发性能要求不是极端的场景。
不适用场景:跨异构数据库(MySQL+MongoDB)、涉及非关系型存储的操作、批量大量更新(undo_log膨胀严重)、对数据库触发器/存储过程的修改。这些场景需要使用TCC模式或Saga模式。
API接口规范方面,Seata的@GlobalTransactional只能标注在public方法上,且不支持类级别标注。如果同一个类内部方法调用(self-invocation),@GlobalTransactional不会生效——这是Spring AOP代理的限制,与@Transactional的行为一致。服务治理中需要通过Bean注入或AopContext.currentProxy()解决自调用问题。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/seata-fen-bu-shi-shi-wu-shi-zhan-at-mo-shi-zai-springboot/