AT简介
-
AT模式是基于XA演进而来的一种分布式事务模式,是Seata主推的分布式事务解决方案。它也分为三大模块,分别是TM、RM和TC,TM和RM作为客户端与业务系统集成,TC作为Seata服务器单独部署
-
AT模式采用的是 Write Ahead Log 思想,即把事务的信息以事务日志的方式记录下来。
-
概念
- TM(Transaction Manager):表示事务管理器,负责向TC注册一个全局事务,并生成一个全局唯一的XID。
- RM(Resource Manager):表示数据库资源,业务层通过JDBC接口访问RM时,Seata会对所有请求进行拦截。每个本地事务进行提交时,RM都会向TC(Transaction Coordinator事务协调器)注册一个分支事务
- TC(Transaction Coordinator): 这是一个独立的服务,是一个独立的 JVM 进程,里面不包含任何业务代码,它的主要职责:维护着整个事务的全局状态,负责通知 RM 执行回滚或提交;
AT模式原理
-
AT模式工作流程分为两个阶段
-
一阶段:(1)拦截并解析SQL,查询前置快照。(2)执行业务SQL。(3)查询后置快照。(4)将前置快照和后置快照数据整合并生成undo Log。(5)向服务端注册分支事务 (6)插入Undo Log ,提交事务
-
二阶段(提交):一阶段成功,二阶段插入待删除队列,异步删除一阶段的Undo Log
-
二阶段(回滚):生成反向SQL,删除Undo log
-
AT项目实战
-
项目源码:https://github.com/jannal/transaction/tree/master/seata-at
-
服务模块
服务 描述 service-account 账户服务 service-points 积分服务 service-aggregation 聚合服务
账户服务
-
SQL脚本
CREATE DATABASE IF NOT EXISTS seata_account DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_bin; DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT '用户唯一标识', `username` varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT '用户名', `password` varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT '密码', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户'; DROP TABLE IF EXISTS `undo_log`; 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=utf8mb4;
-
服务接口
public interface AccountFacadeService { public void registerUser(UserRequestDTO userRequestDTO); } @Slf4j @DubboService(version = "1.0.0") public class AccountFacadeServiceImpl implements AccountFacadeService { @Autowired private UserService userService; @Override public void registerUser(UserRequestDTO userRequestDTO) { log.info("全局事务id:{}", RootContext.getXID()); User user = new User(); BeanUtils.copyProperties(userRequestDTO, user); userService.register(user); } }
-
配置代理数据源
@Configuration @Slf4j public class DataSourceProxyConfig { @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception { log.info("代理的原始数据源类:{}", dataSource.getClass().getName()); //seata数据源代理类代理原始的DataSource DataSourceProxy dataSourceProxy = new DataSourceProxy(dataSource); SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mapper/*Mapper.xml")); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
-
seata配置
seata.enabled=true #写成这样不生效 #seata.enable-auto-data-source-proxy=true #seata.enableAutoDataSourceProxy=true seata.application-id=account-provider-seata seata.registry.type=nacos # Server和Client端的值需一致,默认seata-server seata.registry.nacos.application=seata-server seata.registry.nacos.server-addr=192.168.101.8:8848 seata.registry.nacos.group=DEFAULT_GROUP seata.registry.nacos.cluster=default seata.registry.nacos.username=root seata.registry.nacos.password=root seata.registry.nacos.namespace=e489e0de-8001-41b8-83a6-3241d426a9f7 seata.config.type=nacos seata.config.nacos.data-id=seataServer.properties seata.config.nacos.server-addr=192.168.101.8:8848 seata.config.nacos.group=DEFAULT_GROUP seata.config.nacos.username=root seata.config.nacos.password=root seata.config.nacos.namespace=e489e0de-8001-41b8-83a6-3241d426a9f7 # nacos server默认值是my_test_tx_group seata.tx-service-group=my_test_tx_group seata.service.vgroup-mapping.my_test_tx_group=default seata.service.disable-global-transaction=false seata.data-source-proxy-mode=AT
积分服务
-
SQL脚本
CREATE DATABASE IF NOT EXISTS seata_points DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_bin; DROP TABLE IF EXISTS `t_points`; CREATE TABLE `t_points` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` varchar(50) COLLATE utf8mb4_bin NOT NULL COMMENT '用户唯一标识', `num` bigint(20) NOT NULL COMMENT '积分', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uniq_userid` (`user_id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='积分'; DROP TABLE IF EXISTS `undo_log`; 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=utf8mb4;
-
接口
public interface PointsFacadeService { public void increasePoints(PointsRequestDTO pointsRequestDTO); } @DubboService(version = "1.0.0") @Slf4j public class PointsFacadeServiceImpl implements PointsFacadeService { @Autowired private PointsService pointsService; @Override public void increasePoints(PointsRequestDTO pointsRequestDTO) { log.info("全局事务id:{}", RootContext.getXID()); Points points = new Points(); BeanUtils.copyProperties(pointsRequestDTO, points); pointsService.increasePoints(points); } }
-
配置代理数据源
@Configuration @Slf4j public class DataSourceProxyConfig { @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception { log.info("代理的原始数据源类:{}", dataSource.getClass().getName()); //seata数据源代理类代理原始的DataSource DataSourceProxy dataSourceProxy = new DataSourceProxy(dataSource); SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSourceProxy); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mapper/*Mapper.xml")); sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory()); return sqlSessionFactoryBean.getObject(); } }
-
seata配置
seata.enabled=true #写成这样不生效 #seata.enable-auto-data-source-proxy=true #seata.enableAutoDataSourceProxy=true seata.application-id=points-provider-seata seata.registry.type=nacos # Server和Client端的值需一致,默认seata-server seata.registry.nacos.application=seata-server seata.registry.nacos.server-addr=192.168.101.8:8848 seata.registry.nacos.group=DEFAULT_GROUP seata.registry.nacos.cluster=default seata.registry.nacos.username=root seata.registry.nacos.password=root seata.registry.nacos.namespace=e489e0de-8001-41b8-83a6-3241d426a9f7 seata.config.type=nacos seata.config.nacos.data-id=seataServer.properties seata.config.nacos.server-addr=192.168.101.8:8848 seata.config.nacos.group=DEFAULT_GROUP seata.config.nacos.username=root seata.config.nacos.password=root seata.config.nacos.namespace=e489e0de-8001-41b8-83a6-3241d426a9f7 # nacos server默认值是my_test_tx_group seata.tx-service-group=my_test_tx_group seata.service.vgroup-mapping.my_test_tx_group=default seata.service.disable-global-transaction=false seata.data-source-proxy-mode=AT
聚合服务
-
代码
@Service @Slf4j public class RegisterAggregationService { @DubboReference(version = "1.0.0") private AccountFacadeService accountFacadeService; @DubboReference(version = "1.0.0") private PointsFacadeService pointsFacadeService; /** * 注册用户,增加积分 */ @GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 200000) public void register(UserRequestDTO userRequestDTO, PointsRequestDTO pointsRequestDTO) { log.info("全局事务id:{}", RootContext.getXID()); // 为了模拟注册失败,将增加积分放在前面。测试注册失败,积分是否会增加 pointsFacadeService.increasePoints(pointsRequestDTO); accountFacadeService.registerUser(userRequestDTO); } }
-
seata配置
seata.enabled=true seata.enable-auto-data-source-proxy=true seata.application-id=${spring.application.name}-seata seata.registry.type=nacos # Server和Client端的值需一致,默认seata-server seata.registry.nacos.application=seata-server seata.registry.nacos.server-addr=192.168.101.8:8848 seata.registry.nacos.group=DEFAULT_GROUP seata.registry.nacos.cluster=default seata.registry.nacos.username=root seata.registry.nacos.password=root seata.registry.nacos.namespace=e489e0de-8001-41b8-83a6-3241d426a9f7 seata.config.type=nacos seata.config.nacos.data-id=seataServer.properties seata.config.nacos.server-addr=192.168.101.8:8848 seata.config.nacos.group=DEFAULT_GROUP seata.config.nacos.username=root seata.config.nacos.password=root seata.config.nacos.namespace=e489e0de-8001-41b8-83a6-3241d426a9f7 # nacos server默认值是my_test_tx_group seata.tx-service-group=my_test_tx_group seata.service.vgroup-mapping.my_test_tx_group=default seata.service.disable-global-transaction=false seata.data-source-proxy-mode=AT
查看回滚日志
-
发送请求
curl -H "Content-Type: application/json" -X POST \ -d '{"username": "tom","password": "123456"}' \ http://127.0.0.1:9000/api/v1/register
-
在
AbstractUndoLogManager#batchDeleteUndoLog
出打断点 -
查看account回滚日志,回滚日志记录了业务操作前后的数据,当要执行全局事务回滚时,根据回滚日志进行补偿即可
-
查看回滚日志JSON内容:
{ "@class": "io.seata.rm.datasource.undo.BranchUndoLog", "xid": "172.26.0.2:8091:18275457457209345", "branchId": 18275457457209351, "sqlUndoLogs": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.undo.SQLUndoLog", "sqlType": "INSERT", "tableName": "t_user", "beforeImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords", "tableName": "t_user", "rows": [ "java.util.ArrayList", [] ] }, "afterImage": { "@class": "io.seata.rm.datasource.sql.struct.TableRecords", "tableName": "t_user", "rows": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.sql.struct.Row", "fields": [ "java.util.ArrayList", [ { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "id", "keyType": "PRIMARY_KEY", "type": -5, "value": [ "java.math.BigInteger", 24 ] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "user_id", "keyType": "NULL", "type": 12, "value": "d131c0c4-c415-4f3b-9efd-ce6aa39be439" }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "username", "keyType": "NULL", "type": 12, "value": "tom" }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "password", "keyType": "NULL", "type": 12, "value": "123456" }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "create_time", "keyType": "NULL", "type": 93, "value": [ "java.sql.Timestamp", [ 1652170374000, 0 ] ] }, { "@class": "io.seata.rm.datasource.sql.struct.Field", "name": "update_time", "keyType": "NULL", "type": 93, "value": [ "java.sql.Timestamp", [ 1652170374000, 0 ] ] } ] ] } ] ] } } ] ] }