AT模式是基于XA演进而来的一种分布式事务模式,是Seata主推的分布式事务解决方案。它也分为三大模块,分别是TM、RM和TC,TM和RM作为客户端与业务系统集成,TC作为Seata服务器单独部署
AT模式采用的是 Write Ahead Log 思想,即把事务的信息以事务日志的方式记录下来。
概念
AT模式工作流程分为两个阶段
一阶段:(1)拦截并解析SQL,查询前置快照。(2)执行业务SQL。(3)查询后置快照。(4)将前置快照和后置快照数据整合并生成undo Log。(5)向服务端注册分支事务 (6)插入Undo Log ,提交事务
二阶段(提交):一阶段成功,二阶段插入待删除队列,异步删除一阶段的Undo Log
二阶段(回滚):生成反向SQL,删除Undo log
项目源码: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
出打断点
查看回滚日志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
]
]
}
]
]
}
]
]
}
}
]
]
}