• 分布式事务之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;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
    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);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    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();
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    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
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26

    积分服务

    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;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
    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);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    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();
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    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
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26

    聚合服务

    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);
      
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
    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
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24

    查看回滚日志

    1. 发送请求

      curl -H "Content-Type: application/json" -X POST \
      -d '{"username": "tom","password": "123456"}'  \
      http://127.0.0.1:9000/api/v1/register
      
      • 1
      • 2
      • 3
    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
                                                      ]
                                                  ]
                                              }
                                          ]
                                      ]
                                  }
                              ]
                          ]
                      }
                  }
              ]
          ]
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
  • 相关阅读:
    C++设计模式 - 访问器模式(Visitor)
    阿里云国际放行DDoS高防回源IP
    社恐适合什么工作?能做自媒体吗?
    02-Linux
    JNI中javah命令的使用,生成.h的头文件
    将DataFrame中object类型的列转换为更恰当的数据类型df.infer_objects()
    【Qt炫酷动画】demo03-仿web消去的消息框动画
    Kubernetes源码阅读环境搭建
    嵌入式软件开发工程师未来的薪资待遇是什么情况
    接口自动化测试很难吗?来看看这份超详细的教程!
  • 原文地址:https://blog.csdn.net/usagoole/article/details/126162735