分布式事务之Seata AT

AT简介

  1. AT模式是基于XA演进而来的一种分布式事务模式,是Seata主推的分布式事务解决方案。它也分为三大模块,分别是TM、RM和TC,TM和RM作为客户端与业务系统集成,TC作为Seata服务器单独部署

  2. AT模式采用的是 Write Ahead Log 思想,即把事务的信息以事务日志的方式记录下来。

  3. 概念

    • TM(Transaction Manager):表示事务管理器,负责向TC注册一个全局事务,并生成一个全局唯一的XID。
    • RM(Resource Manager):表示数据库资源,业务层通过JDBC接口访问RM时,Seata会对所有请求进行拦截。每个本地事务进行提交时,RM都会向TC(Transaction Coordinator事务协调器)注册一个分支事务
    • TC(Transaction Coordinator): 这是一个独立的服务,是一个独立的 JVM 进程,里面不包含任何业务代码,它的主要职责:维护着整个事务的全局状态,负责通知 RM 执行回滚或提交;

AT模式原理

  1. AT模式工作流程分为两个阶段

    • 一阶段:(1)拦截并解析SQL,查询前置快照。(2)执行业务SQL。(3)查询后置快照。(4)将前置快照和后置快照数据整合并生成undo Log。(5)向服务端注册分支事务 (6)插入Undo Log ,提交事务

    • 二阶段(提交):一阶段成功,二阶段插入待删除队列,异步删除一阶段的Undo Log

    • 二阶段(回滚):生成反向SQL,删除Undo log

AT项目实战

  1. 项目源码:https://github.com/jannal/transaction/tree/master/seata-at

  2. 服务模块

    服务描述
    service-account账户服务
    service-points积分服务
    service-aggregation聚合服务

账户服务

  1. 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;
    
  2. 服务接口

    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);
        }
    }
    
  3. 配置代理数据源

    @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();
        }
    
    }
    
  4. 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
    

积分服务

  1. 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;
    
  2. 接口

    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);
        }
    }
    
  3. 配置代理数据源

    @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();
        }
    
    }
    
  4. 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
    

聚合服务

  1. 代码

    @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);
    
        }
    }
    
  2. 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
    

查看回滚日志

  1. 发送请求

    curl -H "Content-Type: application/json" -X POST \
    -d '{"username": "tom","password": "123456"}'  \
    http://127.0.0.1:9000/api/v1/register
    
  2. AbstractUndoLogManager#batchDeleteUndoLog出打断点

  3. 查看account回滚日志,回滚日志记录了业务操作前后的数据,当要执行全局事务回滚时,根据回滚日志进行补偿即可
    在这里插入图片描述

  4. 查看回滚日志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
                                                    ]
                                                ]
                                            }
                                        ]
                                    ]
                                }
                            ]
                        ]
                    }
                }
            ]
        ]
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值