10.seata解决分布式事务

概述

seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

有阿里巴巴背书,该框架的活跃度最近几年活跃度非常高。

https://github.com/seata/seata seata官网

https://yq.aliyun.com/zt/593075 阿里云GTS

目前授课版本Seata1.4版本 https://github.com/seata/seata/releases/tag/v1.4.2

10.seata解决分布式事务_第1张图片

回顾知识点

1.redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。

2.undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。

AT模式

基于支持本地 ACID 事务的关系型数据库。

Java 应用,通过 JDBC 访问数据库。

两阶段提交协议的演变:

一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

二阶段:

提交异步化,非常快速地完成。

回滚通过一阶段的回滚日志进行反向补偿。

AT原理

tc----事务协调者

tm—lcn发起方 支付服务

rm—积分服务 lcn参与方

Seata有3个基本组成部分:

1.事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚,相当于是协调者。

2.事务管理器TM:定义全局事务的范围:开始全局事务,提交或回滚全局事务,相当于LCN中发起方。

3.资源管理器(RM):管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚,相当于是LCN中的参与方

概念名称:

Transaction ID (XID) : 全局唯一的事务ID

Transaction Coordinator (TC) : 事务协调器,维护全局事务的运行状态

Transaction Manager ™:控制全局事务的边界,负责开启一个全局事务

Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接受TC的提交或者回滚操作

1.TM(发起方)连接到我们的TC事务协调者,创建一个全局的事务的xid,保存到ThreadLocal中;

2.TM(发起方)和RM(参与方)都被Seata的数据数据源实现代理,在原生的sql之前和之后保存原来和修改后日志到undo_log中,方便后期实现回滚。
3.TM(发起方)使feign客户端调用接口时候,在ThreadLocal中获取xid,设置到请求头中;

4.RM(参与方)从请求中获取到该xid,设置到ThreadLoacl中,同时也会向seataserver注册该分支事务。

5.TM(发起方)将当前本地事务的结果,告诉给协调者TC,协调者TC在通知所有的分支是否回滚。

\6. TM(发起方)如果调用接口成功之后抛出异常的情况下,告诉给协调者TC,协调者TC在通知所有的分支根据根据全局的xid和分支事务的id 查询分支数据源的undo_log日志表逆向生成sql语句实现回滚,同时删除对应的undo_log日志

\7. TM(发起方)如果调用接口成功之后没有抛出任何的异常,告诉给协调者TC,协调者TC在通知所有的分支根据根据全局的xid和分支事务的id 查询分支数据源的 删除对应的undo_log日志表

10.seata解决分布式事务_第2张图片

事务分组专题

事务分组是什么?

1.事务分组:seata的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。

2.集群:seata-server服务端一个或多个节点组成的集群cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与Seata服务端集群的映射关系。

事务分组如何找到后端Seata集群?

  1. 首先应用程序(客户端)中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数)。若应用程序是SpringBoot则通过seata.tx-service-group 配置
  2. 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping .[事务分组配置项],取得配置项的值就是TC集群的名称。若应用程序是SpringBoot则通过seata.service.vgroup-mapping.事务分组名=集群名称 配置
  3. 拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同(前提是Seata-Server已经完成服务注册,且Seata-Server向注册中心报告cluster名与应用程序(客户端)配置的集群名称一致)
  4. 拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表(即Seata-Server集群节点列表)

tc事务协调者如何保证高可用

https://seata.io/zh-cn/docs/user/txgroup/transaction-group-and-ha.html

  • 假定TC集群部署在两个机房:guangzhou机房(主)和shanghai机房(备)各两个实例
  • 一整套微服务架构项目:projectA
  • projectA内有微服务:serviceA、serviceB、serviceC 和 serviceD

其中,projectA所有微服务的事务分组tx-service-group设置为:projectA,projectA正常情况下使用guangzhou的TC集群(主)

那么正常情况下,client端的配置如下所示:

seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou

10.seata解决分布式事务_第3张图片

假如此时guangzhou集群分组整个down掉,或者因为网络原因projectA暂时无法与Guangzhou机房通讯,那么我们将配置中心中的Guangzhou集群分组改为Shanghai,如下:

seata.service.vgroup-mapping.projectA=Shanghai 

并推送到各个微服务,便完成了对整个projectA项目的TC集群动态切换。

10.seata解决分布式事务_第4张图片

TC事务协调者环境搭建

目前授课版本Seata1.4.2 版本 https://github.com/seata/seata/releases/tag/v1.4.2

10.seata解决分布式事务_第5张图片

seata Server 端 (tc)环境搭建 支持三种模式

1.file:单机模式 数据保存在本地文件,不支持高可用 不推荐使用。 root.data

2.db: 高可用模式 数据共享保存在 db中 数据库要求(5.7以上的版本)

3.redis: redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.

分布式事务演示表结构

CREATE TABLE `mayikt_payinfo` (
  `id` int(11) NOT NULL,
  `pay_id` int(11) DEFAULT NULL,
  `pay_name` varchar(255) DEFAULT NULL,
  `pay_state` int(11) DEFAULT NULL,
  `pay_money` decimal(10,0) DEFAULT NULL,
  `pay_userid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `mayikt_integral_info` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `value` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `pay_id` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8;

初始化db

创建Seata db

/*
Navicat MySQL Data Transfer

Source Server         : 127.0.0.1
Source Server Version : 50717
Source Host           : 127.0.0.1:3306
Source Database       : mayikt-seata

Target Server Type    : MYSQL
Target Server Version : 50717
File Encoding         : 65001

Date: 2022-01-04 19:47:38
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `lock_key` varchar(128) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of branch_table
-- ----------------------------

-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) DEFAULT NULL,
  `transaction_service_group` varchar(32) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of global_table
-- ----------------------------

-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,
  `xid` varchar(96) DEFAULT NULL,
  `transaction_id` mediumtext,
  `branch_id` mediumtext,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(36) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of lock_table
-- ----------------------------

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) 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(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime NOT NULL COMMENT 'create datetime',
  `log_modified` datetime NOT NULL COMMENT 'modify datetime',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

-- ----------------------------
-- Records of undo_log
-- ----------------------------

Seata db中:

1.global_table: 存放全局的事务信息

2.branch_table :存入参与者分支事务信息

修改配置文件

1.file.conf 配置缓存我们Seata数据 存储模式 store mode: file、db、redis

2.registry.conf 配置Seata服务注册

3.目前搭建的环境为Seata1.4版本,配置相比往期减少很多。

4.在config目录:D:\path\cloud\seata-server-1.4.2\seata\seata-server-1.4.2\conf

file.conf store mode改为db registry.conf 为nacos

store mode改为db

10.seata解决分布式事务_第6张图片

10.seata解决分布式事务_第7张图片

registry.conf 为nacos

10.seata解决分布式事务_第8张图片

group = “SEATA_GROUP” 连接 nacos 分组:SEATA_GROUP 而

我们nacos 默认的分组名称:DEFAULT_GROUP

注意事项:以后新增SEATA 相关配置文件内容,注意 一定要是

SEATA_GROUP 分组,否则读取不到该配置文件内容。

application = “seata-server” 注册nacos 服务名称

修改配置中心地址

10.seata解决分布式事务_第9张图片

nacos-config.sh上传配置文件

1.将seata config.txt 配置上传到nacos中,在nacos中创建一个新的命名空间为 mayikt-seata

10.seata解决分布式事务_第10张图片

2.获取命名空间id:6504a6f4-75b2-4653-8708-cffbecc639c8

(seata版本中是没有这个文件,需要单独下载的 )

nacos-config.sh

3.将nacos-config.sh文件拷贝到:

D:\path\cloud\seata-server-1.4.2\seata\seata-server-1.4.2\conf 目录中

10.seata解决分布式事务_第11张图片

  1. 在seata-server-1.4.2根目录 创建一个config.txt文件

文件内容:(主要修改成自己的db信息)

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

10.seata解决分布式事务_第12张图片

config.txt

5.打开cmd 进入 D:\path\cloud\seata-server-1.4.2\seata执行

nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 2054d211-594c-4f62-9622-ffaeeff85f37 -u nacos -w nacos

nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 9a2fd6c4-ee9c-47d8-98db-fd0045ae2360 -u nacos -w nacos

注:命令解析:-h -p 指定nacos的端口地址;-g 指定配置的分组,注意,是配置的分组;-t 指定命名空间id; -u -w指定nacos的用户名和密码,同样,这里开启了nacos注册和配置认证的才需要指定。

注意需要修改:config.txt db为自己db连接信息

启动当前seata Server项目

img

6.配置文件上传到nacos中成功

10.seata解决分布式事务_第13张图片

\7. 登录nacos 平台 选择 mayikt-seata 命名空间 配置文件上传成功

10.seata解决分布式事务_第14张图片

启动seataServer

D:\path\cloud\seata-server-1.4.2\seata\seata-server-1.4.2\bin

seata-server.bat 双击启动即可

查看nacos 服务管理

10.seata解决分布式事务_第15张图片

客户端整合

客户端整合过程中,如果启动发生错误 大多数原因是由于 springboot与seata版本不一致问题导致。

明确说明当前环境版本: seata-server-1.4.2 、springboot 2.2.3.RELEAS spring-cloud-starter-openfeign 2.2.3.RELEASE

新增配置文件

seata:
  enabled: true
  enable-auto-data-source-proxy: true
  tx-service-group: my_test_tx_group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
      namespace: a90e9c9c-60c3-44d7-a304-a13496f8ea01
  service:
    vgroup-mapping:
      my_test_tx_group: default
    disable-global-transaction: false
  client:
    rm:
      report-success-enable: false

my_test_tx_group 是可以自定义的 例如 改成:mayikt-cloud

server:
  port: 7050
spring:
  application:
    name: mayikt-pay  #服务名称 在 注册中心展示服务名称 --
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # nacos服务注册中心Server端 地址
    refresh:
      refreshable: none
  datasource: ##prd_mayikt
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/mayikt-pay?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT
    username: root
    password: root
seata:
  enabled: true
  enable-auto-data-source-proxy: true
  tx-service-group: mayikt-cloud
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
      namespace: 2054d211-594c-4f62-9622-ffaeeff85f37
  service:
    vgroup-mapping:
      mayikt-cloud: default
    disable-global-transaction: false
  client:
    rm:
      report-success-enable: false

10.seata解决分布式事务_第16张图片

10.seata解决分布式事务_第17张图片

同时在 nacos -config 配置:

service.vgroupMapping.mayikt-cloud:default

注意分组是:SEATA_GROUP

registry.conf 配置文件:

cluster = “default”

否则会报错:

参考seata github: https://github.com/seata/seata/issues/2406

Maven依赖

     <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>

启动项目

在接口上加上@GlobalTransactional,启动项目

会显示 rm服务注册成功。

10.seata解决分布式事务_第18张图片

查询nacos 服务注册中心

10.seata解决分布式事务_第19张图片

undolog日志表

{
	"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
	"xid": "169.254.132.125:8091:1990807513062101344",
	"branchId": 1990807513062101346,
	"sqlUndoLogs": ["java.util.ArrayList", [{
		"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
		"sqlType": "UPDATE",
		"tableName": "mayikt_payinfo",
		"beforeImage": {
			"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
			"tableName": "mayikt_payinfo",
			"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": 4,
					"value": 1
				}, {
					"@class": "io.seata.rm.datasource.sql.struct.Field",
					"name": "pay_state",
					"keyType": "NULL",
					"type": 4,
					"value": 0
				}]]
			}]]
		},
		"afterImage": {
			"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
			"tableName": "mayikt_payinfo",
			"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": 4,
					"value": 1
				}, {
					"@class": "io.seata.rm.datasource.sql.struct.Field",
					"name": "pay_state",
					"keyType": "NULL",
					"type": 4,
					"value": 1
				}]]
			}]]
		}
	}]]
}

每数据库需要单独创建

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` varchar(3000) NOT NULL COMMENT 'rollback info',
  `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime NOT NULL COMMENT 'create datetime',
  `log_modified` datetime NOT NULL COMMENT 'modify datetime',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

seata at模式源码解读

如何生成全局唯一id

  1. GlobalTransactionalInterceptor#invoke方法, 判断方法上是否有加上GlobalTransactional注解,如存在的话,则走handleGlobalTransaction 方法 执行transactionalTemplate.execute方法 。
 @Override
    public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
        Class<?> targetClass =
            methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;
        Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
        if (specificMethod != null && !specificMethod.getDeclaringClass().equals(Object.class)) {
            final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
            final GlobalTransactional globalTransactionalAnnotation =
                getAnnotation(method, targetClass, GlobalTransactional.class);
            final GlobalLock globalLockAnnotation = getAnnotation(method, targetClass, GlobalLock.class);
            boolean localDisable = disable || (degradeCheck && degradeNum >= degradeCheckAllowTimes);
            if (!localDisable) {
                if (globalTransactionalAnnotation != null) {
                    return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);
                } else if (globalLockAnnotation != null) {
                    return handleGlobalLock(methodInvocation);
                }
            }
        }
        return methodInvocation.proceed();
    }

2.beginTransaction 方法()连接到全局事务协调者创建一个,全局的xid;transactionManager.begin发送请求,申请全局的id,底层基于netty封装的,缓存到threadlocal中。

 try {

                // 2. begin transaction
                beginTransaction(txInfo, tx);

                Object rs = null;
                try {

                    // Do Your Business
                    rs = business.execute();

                } catch (Throwable ex) {

                    // 3.the needed business exception to rollback.
                    completeTransactionAfterThrowing(txInfo, tx, ex);
                    throw ex;
                }

                // 4. everything is fine, commit.
                commitTransaction(tx);

                return rs;
            } finally {
                //5. clear
                triggerAfterCompletion();
                cleanUp();
            }

Seata如何传递xid的?

SeataFeignClient 重写了 feign客户端client,从threadlocal中获取xid,

放入到请求头中。

	private Request getModifyRequest(Request request) {

		String xid = RootContext.getXID();

		if (StringUtils.isEmpty(xid)) {
			return request;
		}

		Map<String, Collection<String>> headers = new HashMap<>(MAP_SIZE);
		headers.putAll(request.headers());

		List<String> seataXid = new ArrayList<>();
		seataXid.add(xid);
		headers.put(RootContext.KEY_XID, seataXid);

		return Request.create(request.method(), request.url(), headers, request.body(),
				request.charset());
	}

Seata如何生成前置与后置镜像

1.通过日志分析法,将undo-log 表删除,执行完insert之后向undo-log表中插入一条数据报错,该表不存在。

2.日志可以分析出:在MySQLUndoLogManager.insertUndoLog插入一条undolog日志,插入完毕之后并且提交数据。

3.实现原理:seata会创建一个代理数据源,对我们数据源前后实现操作,在我们执行sql语句完成之后插入一条undolog日志。

生成undolog日志:AbstractDMLBaseExecutor#executeAutoCommitFalse方法。

4.Seata如何生成前置与后置镜像;

通过日志可以分析得出:

基于代理数据源的形式,对原生sql语句插入一条undolog日志

前置与后置镜像。

ConnectionProxy#doCommit提交,在AbstractUndoLogManager.flushUndoLogs方法中插入一条前置和后置镜像记录,转换为字节的形式存储到db中。

ConnectionProxy#processGlobalTransactionCommit 插入一条前置和后置镜像。

Seata如何接受全局id

Seata参与方 使用拦截器 接受feign客户端从请求中传递的

TransactionPropagationIntercepter 从请求头获取全局的事务xid,放入到threadlocal中。

 @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String xid = RootContext.getXID();
        String rpcXid = request.getHeader(RootContext.KEY_XID);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("xid in RootContext[{}] xid in HttpContext[{}]", xid, rpcXid);
        }
        if (rpcXid != null) {
            RootContext.bind(rpcXid);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("bind[{}] to RootContext", rpcXid);
            }
        }


        return true;
    }

Seata如何实现逆向回滚

\1. 如果发起方没有报错的情况下, 发起方通知给tc事务协调者,tc事务协调者在通知给参与方,删除该undolog日志。

2.如果发起方失败的情况下,则会查询本地数据库中的undolog日志,根据undolog日志逆向实现回滚数据DataSourceManager# branchRollback方法中。

10.seata解决分布式事务_第20张图片

public abstract class AbstractRMHandler extends AbstractExceptionHandler
    implements RMInboundHandler, TransactionMessageHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRMHandler.class);

    @Override
    public BranchCommitResponse handle(BranchCommitRequest request) {
        BranchCommitResponse response = new BranchCommitResponse();
        exceptionHandleTemplate(new AbstractCallback<BranchCommitRequest, BranchCommitResponse>() {
            @Override
            public void execute(BranchCommitRequest request, BranchCommitResponse response)
                throws TransactionException {
                doBranchCommit(request, response);
            }
        }, request, response);
        return response;
    }

    @Override
    public BranchRollbackResponse handle(BranchRollbackRequest request) {
        BranchRollbackResponse response = new BranchRollbackResponse();
        exceptionHandleTemplate(new AbstractCallback<BranchRollbackRequest, BranchRollbackResponse>() {
            @Override
            public void execute(BranchRollbackRequest request, BranchRollbackResponse response)
                throws TransactionException {
                doBranchRollback(request, response);
            }
        }, request, response);
        return response;
    }
    @Override
    public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId,
                                       String applicationData) throws TransactionException {
        DataSourceProxy dataSourceProxy = get(resourceId);
        if (dataSourceProxy == null) {
            throw new ShouldNeverHappenException();
        }
        try {
            UndoLogManagerFactory.getUndoLogManager(dataSourceProxy.getDbType()).undo(dataSourceProxy, xid, branchId);
        } catch (TransactionException te) {
            StackTraceLogger.info(LOGGER, te,
                "branchRollback failed. branchType:[{}], xid:[{}], branchId:[{}], resourceId:[{}], applicationData:[{}]. reason:[{}]",
                new Object[]{branchType, xid, branchId, resourceId, applicationData, te.getMessage()});
            if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
                return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
            } else {
                return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
            }
        }
        return BranchStatus.PhaseTwo_Rollbacked;

    }
 public static AbstractUndoExecutor getUndoExecutor(String dbType, SQLUndoLog sqlUndoLog) {
        AbstractUndoExecutor result = null;
        UndoExecutorHolder holder = UndoExecutorHolderFactory.getUndoExecutorHolder(dbType.toLowerCase());
        switch (sqlUndoLog.getSqlType()) {
            case INSERT:
                result = holder.getInsertExecutor(sqlUndoLog);
                break;
            case UPDATE:
                result = holder.getUpdateExecutor(sqlUndoLog);
                break;
            case DELETE:
                result = holder.getDeleteExecutor(sqlUndoLog);
                break;
            default:
                throw new ShouldNeverHappenException();
        }
        return result;
    }

DELETE FROM mayikt-integral.mayikt_integral_info WHERE id = ?

10.seata解决分布式事务_第21张图片

整合seata常见坑

检查事务中的表tablename是否有主键

java.sql.SQLException: Failed to fetch schema of tablename

Caused by: io.seata.common.exception.ShouldNeverHappenException: Could not found any index in the table: tablename

io.seata.common.exception.ShouldNeverHappenException: [xid:10.2.30.38:30109:567626512211157189]get tablemeta failed

解决方案:

检查事务中的表tablename是否有主键(没有主键会报错)

如果在seata在逆向回滚前数据已经被其他线程发生变更 如何处理?

支付调用完积分服务 积分增加成功

插入前置镜像—null

insert(id=58)

插入后置镜像----id=58 积分=3000

另外线程修改 新增的积分

–id=58 积分=1

支付服务报错呢?—回滚失败的呢?

比对 undolog日志 存放后置镜像与当前数据不一致?

还原 删除。

会一直抛出该异常: 回滚失败

原因:undolog 后置镜像存放的日志 更新后的数据 被另外线程已经做了更改了。

cationData=null
2022-01-06 16:14:57.578  INFO 83320 --- [_RMROLE_1_24_32] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 169.254.132.125:8091:3242808895986365004 3242808895986365006 jdbc:mysql://127.0.0.1:3306/mayikt-pay
2022-01-06 16:14:57.579  INFO 83320 --- [_RMROLE_1_24_32] i.s.r.d.undo.AbstractUndoExecutor        : Field not equals, name pay_state, old value 1, new value 0
2022-01-06 16:14:57.579  INFO 83320 --- [_RMROLE_1_24_32] i.seata.rm.datasource.DataSourceManager  : branchRollback failed. branchType:[AT], xid:[169.254.132.125:8091:3242808895986365004], branchId:[3242808895986365006], resourceId:[jdbc:mysql://127.0.0.1:3306/mayikt-pay], applicationData:[null]. reason:[Branch session rollback failed and try again later xid = 169.254.132.125:8091:3242808895986365004 branchId = 3242808895986365006 Has dirty records when undo.]

undo log日志 已经存在

img

解决办法:

1.还原数据(不靠谱)

2.乐观锁

3.手动删除undolog日志

4.开启本地事务

上课笔记

第一天

分布式事务框架

底层代理数据源

数据源执行sql语句前后 处理操作--------aop。

AOP 只代理 方法—不对的。

前置镜像----在 写操作之前数据

后置镜像-----写操作之后数据

前置镜像 ----select pay_state,id 主键id from mayikt_payinfo where where pay_id = 1 原来的状态值=0;

json格式

update mayikt_payinfo set pay_state= ‘1’ where pay_id = 1----执行 update

后置镜像

根据前置镜像的 主键id 查询

select pay_state from mayikt_payinfo where where id=1;

pay_state===1

前置和后置镜像保存起来 并且提交事务。

前置和后置镜像 插入到 undolog表中(全局事务id)。-------与数据源挂钩

前置镜像 1 原来的状态值=0; 逆向回滚

update mayikt_payinfo set pay_state= ‘0’ where id= 1----执行 update

逆向回滚:

执行update 语句—update 根据前置镜像逆向回滚

执行delete id=语句 ----insert(-----逻辑删除 隐藏)

执行insert 语句----delete

使用seata 每张表结构必须要有一个主键id-----

没有的话 执行sql语句 生成后置镜像的时候 就会直接报错的。

2pc 在哪些场景下有使用到?

lcn模式 seata at 模式 底层都是 2pc

seata at模式 存在缺陷 脏读的问题

lcn 模式 基于数据源 假关闭

发起方调用参与方接口,参与方事务 不会提交 也不会回滚的。

缺陷:容易引发行锁的问题。

seata at模式 存在缺陷 脏读的问题

底层 根据 undolog日志逆向回滚 ,执行sql语句前后 生成镜像问题。

----

insert-----delete

----查询到该数据

{
    "@class": "io.seata.rm.datasource.undo.BranchUndoLog", 
    "xid": "169.254.132.125:8091:1990807513062101944", 
    "branchId": 1990807513062102000, 
    "sqlUndoLogs": [
        "java.util.ArrayList", 
        [
            {
                "@class": "io.seata.rm.datasource.undo.SQLUndoLog", 
                "sqlType": "INSERT", 
                "tableName": "`mayikt-integral`.`mayikt_integral_info`", 
                "beforeImage": {
                    "@class": "io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords", 
                    "tableName": "mayikt_integral_info", 
                    "rows": [
                        "java.util.ArrayList", 
                        [ ]
                    ]
                }, 
                "afterImage": {
                    "@class": "io.seata.rm.datasource.sql.struct.TableRecords", 
                    "tableName": "mayikt_integral_info", 
                    "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": 4, 
                                            "value": 50
                                        }, 
                                        {
                                            "@class": "io.seata.rm.datasource.sql.struct.Field", 
                                            "name": "value", 
                                            "keyType": "NULL", 
                                            "type": 4, 
                                            "value": 1000
                                        }, 
                                        {
                                            "@class": "io.seata.rm.datasource.sql.struct.Field", 
                                            "name": "user_id", 
                                            "keyType": "NULL", 
                                            "type": 4, 
                                            "value": 123
                                        }, 
                                        {
                                            "@class": "io.seata.rm.datasource.sql.struct.Field", 
                                            "name": "pay_id", 
                                            "keyType": "NULL", 
                                            "type": 12, 
                                            "value": "1"
                                        }
                                    ]
                                ]
                            }
                        ]
                    ]
                }
            }
        ]
    ]
}

TCC模式

at模式----

tcc —

2pc 两阶段

第一阶段 —准备阶段

第二阶段 —提交或者回滚阶段

tcc 需要自己编写代码的形式 提交和回滚。

at模式 开发者不需要写任何代码的

概述

回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

一阶段 prepare 行为

二阶段 commit 或 rollback 行为

10.seata解决分布式事务_第22张图片

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction ModeManual (Branch) Transaction Mode.

AT 模式(参考链接 TBD)基于 支持本地 ACID 事务关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

tcc 框架:1.tcc-transaction 2. seata 支持TCC 模式 3.bytetcc 等。

简单回顾2PC

2PC(两阶段提交协议)

1.两阶段提交协议可以理解为2pc,也就是分为参与者和协调者,协调者会通过两次阶段实现数据最终的一致性的。

2.2PC和3pc的区别就是解决参与者超时的问题和多加了一层询问,保证数据的传输可靠性。

两段提交顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段) ; 第二阶段,提交阶段(执行阶段),中间必须要有一个协调者。

第一节阶段:

协调者会给每个参与者发送一封邮件,如果所有的参与者回复 ok,

如果中间有一个参与者 回复fail。

第二阶段:

就会给每个参与者发送一封确认的邮件,所有参与者发送fail。

生活中例子

例如在我们团队中有 小军、小安、小薇 三个人,打算明天想去聚餐?那如何达成一致呢?

我们就必须选出一名 组长 假设选举 小军为组长 小安和小薇就是为 组员

第一阶段:

小军在给小安和小薇 发一封邮件说“发送邮件:明天晚上7点准时聚餐,你们有时间吗?”

小安和小薇 如果回复“ok”

第二阶段:

小军收到了 小安哥小薇回复的“ok”,就会给 小安和小薇在发一封邮件 说明天晚上7点准时聚餐

小军收到了 小安回复的"ok",小薇回复的“fail”,就会给 小安和小薇在发一封邮件 说明天晚上7点不能够聚餐

小薇没有时间。

第一阶段

我们协调者,会给每一个参与者发送Prepare(预备)消息,执行本地数据脚本但不提交事务;

如果协调者收到了参与者成功的消息,则会给每个参与者发送提交(Commit)消息 , 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,显然2PC做到了所有操作要么全部成功、要么全部失败。

第二阶段

如果协调者收到了参与者成功的消息,则会给每个参与者发送提交(Commit)消息 , 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,显然2PC做到了所有操作要么全部成功、要么全部失败。

10.seata解决分布式事务_第23张图片

10.seata解决分布式事务_第24张图片

2PC缺陷

1、协调者节点挂了此场景比较主流的方案是选举机制,首次即通过选举决定谁是协调者,当协调者挂掉后重新选举。

2、commit阶段出现部分失败

TCC原理

try------资源的检测和预留;

2pc确认

Confirm----提交流程

Cancel----回滚流程

TCC 模式需要根据用户自己的业务场景实现 try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel (Rollback)方法。

注意该业务逻辑是需要我们自己编写代码的。

Try----资源的检测和预留;

Confirm ----执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;

Rollback-----预留资源释放;

Try为第一阶段

Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。

操作方法 含义
Try 预留业务资源/数据效验
Confirm 确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等
Cancel 取消执行业务操作,实际回滚数据,需保证幂等

10.seata解决分布式事务_第25张图片

整合seata

支付服务

package com.mayikt.api.pay.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author 余胜军
 * @ClassName PayService
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */

public interface PayService {
    /**
     * 根据支付id 修改支付状态 是为 已经支付
     *
     * @param payId
     * @return
     */
    @RequestMapping("/updatePaySuccess")
    ResponseEntity updatePaySuccess(Long payId);
}
package com.mayikt.impl.pay.service;

import com.alibaba.fastjson.JSONObject;

import com.mayikt.api.integral.dto.IntegralDto;
import com.mayikt.api.pay.service.PayService;
import com.mayikt.impl.pay.constant.RpcConstant;
import com.mayikt.impl.pay.feign.IntegralServiceFeign;
import com.mayikt.impl.pay.manage.PayServiceManage;
import com.mayikt.impl.pay.mapper.PayServiceMapper;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.spring.annotation.GlobalLock;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 余胜军
 * @ClassName PayServiceImpl
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
@RestController
@Slf4j
public class PayServiceImpl implements PayService {
    @Autowired
    private PayServiceMapper payServiceMapper;
    //    @Autowired
//    private IntegralProduction integralProduction;
    @Autowired
    private IntegralServiceFeign integralServiceFeign;
    @Autowired
    private PayServiceManage payServiceManage;

    @Override
    @GlobalTransactional
    public ResponseEntity updatePaySuccess(Long payId) {
        return payServiceManage.updatePaySuccess(null, payId) ?
                ResponseEntity.status(200).body("ok") : ResponseEntity.status(200).body("fail");
    }


//    @Override
//    @Transactional
//    public ResponseEntity updatePaySuccess(Long payId) {
//        int result = payServiceMapper.updatePaySuccess(payId);
//        if (result <= 0) {
//            return ResponseEntity.status(500).body("fail");
//        }
//        // 使用mq异步形式增加积分
//        IntegralDto integralDto = new IntegralDto();
//        integralDto.setValue(1000l);
//        integralDto.setUserId(123l);
//        integralDto.setPayId(payId);
//        String json = JSONObject.toJSONString(integralDto);
//        log.info("<使用MQ投递消息异步增加积分>");
//        integralProduction.sendIntegralMsg(json);
//        return ResponseEntity.status(200).body("ok");
//    }

//    @Transactional
//    public ResponseEntity updatePaySuccess(Long payId) {
//        // 开启事务
//        // 修改支付表 状态为已经支付成功
//        payServiceMapper.updatePaySuccess(payId); // 数据源 独立事务管理器
//        integralMapper.insertIntegral
//                (integralDto.getValue(), integralDto.getUserId(), integralDto.getPayId());  // 数据源 独立事务管理器
//        int j = 1 / 0;
//        //jta+ Atomikos
//    }
}

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author 余胜军
 * @ClassName PayServiceManage
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
@LocalTCC
public interface PayServiceManage {
    /**
     * 根据支付id 修改支付状态 是为 已经支付
     *
     * @param payId
     * @return
     */
    @TwoPhaseBusinessAction(name = "updatePaySuccess", commitMethod = "commitTcc", rollbackMethod = "cancel")
    boolean updatePaySuccess(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "payId") Long payId);

    /**
     * 确认方法、可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param context 上下文
     * @return boolean
     */
    boolean commitTcc(BusinessActionContext context);

    /**
     * 二阶段取消方法
     *
     * @param context 上下文
     * @return boolean
     */
    boolean cancel(BusinessActionContext context);
}
import com.mayikt.api.integral.dto.IntegralDto;
import com.mayikt.impl.pay.constant.RpcConstant;
import com.mayikt.impl.pay.feign.IntegralServiceFeign;
import com.mayikt.impl.pay.manage.PayServiceManage;
import com.mayikt.impl.pay.mapper.PayServiceMapper;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

/**
 * @author 余胜军
 * @ClassName PayServiceManageImpl
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
@Component
@Slf4j
public class PayServiceManageImpl implements PayServiceManage {
    @Autowired
    private IntegralServiceFeign integralServiceFeign;
    @Autowired
    private PayServiceMapper payServiceMapper;


    public boolean updatePaySuccess(BusinessActionContext actionContext, Long payId) {
        // 1.先查询 该支付id 对应的数据是否存在
        // 2.如果存在的情况下 状态是为 未支付---
        log.info("" + RootContext.inGlobalTransaction());
        int result = payServiceMapper.updatePaySync(payId);
        if (result <= 0) {
//            return ResponseEntity.status(500).body("fail");
        }
        // 使用feign调用积分服务接口增加 积分。
        IntegralDto integralDto = new IntegralDto();
        integralDto.setValue(1000l); // 积分的值1000
        integralDto.setUserId(123l); // 123
        integralDto.setPayId(payId); // 支付的id
        ResponseEntity integralResult = integralServiceFeign.addIntegral(integralDto);
//        // 判断如果调用积分服务接口失败了, 则或回滚当前事务
        if (RpcConstant.RPC_500_CODE.equals(integralResult.getStatusCode())) {
            // 手动回滚当前事务
//            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//            return ResponseEntity.status(500).body("fail");
            return false;
        }
        // 程序执行到 48行 调用完积分服务接口之后 -----积分服务事务提交了。积分服务 对应 积分数据库 积分表 是可以查询到 增加积分数据
        // 模拟代码报错   支付服务调用完积分服务接口之后 突然返回呢?
        if (payId == 1) {
            int j = 1 / 0;  // 支付服务事务回滚了,支付表中状态 未支付状态 但是 积分服务中 积分数据已经提交了。
            // 积分增加---查询到 显示未支付。--解决分布事务问题
        }
        return true;
    }

    @Override
    public boolean commitTcc(BusinessActionContext context) {
        log.info("");
        Integer payId = (Integer) context.getActionContext("payId");
        payServiceMapper.updatePaySuccess(payId);
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        log.info("");
        // 逆向回滚-----将该状态还原... 将状态还原
        Integer payId = (Integer) context.getActionContext("payId");
        payServiceMapper.updatePayNotSuccess(payId);
        return true;
    }

}

积分服务

public interface IntegralService {

    /**
     * 增加积分服务接口
     * @param integralDto
     * @return
     */
    @RequestMapping("/addIntegral")
    ResponseEntity addIntegral(@RequestBody IntegralDto integralDto);

}
import com.mayikt.api.integral.dto.IntegralDto;
import com.mayikt.api.integral.service.IntegralService;
import com.mayikt.impl.integrals.entity.IntegralEntity;
import com.mayikt.impl.integrals.manage.IntegralManage;
import com.mayikt.impl.integrals.mapper.IntegralMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.spring.annotation.GlobalLock;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 余胜军
 * @ClassName IntegralServiceImpl
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
@RestController
@Slf4j
public class IntegralServiceImpl implements IntegralService {
    @Autowired
    private IntegralManage integralManage;
    @GlobalTransactional
    public ResponseEntity addIntegral(@RequestBody IntegralDto integralDto) {
        return integralManage.addIntegral(null, integralDto) ? ResponseEntity.status(200).body("ok") : ResponseEntity.status(500).body("fail");
    }
    
}

import com.mayikt.api.integral.dto.IntegralDto;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @author 余胜军
 * @ClassName IntegralManage
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
@LocalTCC
public interface IntegralManage {
    @TwoPhaseBusinessAction(name = "updatePaySuccess", commitMethod = "commitTcc", rollbackMethod = "cancel")
    boolean addIntegral(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "integralDto") IntegralDto integralDto);

    /**
     * 确认方法、可以另命名,但要保证与commitMethod一致
     * context可以传递try方法的参数
     *
     * @param context 上下文
     * @return boolean
     */
    boolean commitTcc(BusinessActionContext context);

    /**
     * 二阶段取消方法
     *
     * @param context 上下文
     * @return boolean
     */
    boolean cancel(BusinessActionContext context);
}

import com.alibaba.fastjson.JSONObject;
import com.mayikt.api.integral.dto.IntegralDto;
import com.mayikt.impl.integrals.manage.IntegralManage;
import com.mayikt.impl.integrals.mapper.IntegralMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 余胜军
 * @ClassName IntegralManageImpl
 * @qq 644064779
 * @addres www.mayikt.com
 * 微信:yushengjun644
 */
@Component
@Slf4j
public class IntegralManageImpl implements IntegralManage {
    @Autowired
    private IntegralMapper integralMapper;

    @Override
    public boolean addIntegral(BusinessActionContext actionContext, IntegralDto integralDto) {
        try {
            int result = integralMapper.insertIntegral
                    (integralDto.getValue(), integralDto.getUserId(), integralDto.getPayId());
//             update操作
//            int result = integralMapper.updateIntegral(integralDto.getUserId());
//            return result != 0 ? ResponseEntity.status(200).body("ok") : ResponseEntity.status(500).body("fail");
            return true;
        } catch (Exception e) {
            log.error("", e);
            // 积分服务如果报错了 积分服务就已经回滚了 响应状态500
            return false;
        }
    }

    @Override
    public boolean commitTcc(BusinessActionContext context) {
        log.info("");
        JSONObject data = (JSONObject) context.getActionContext("integralDto");
        Integer payId = data.getInteger("payId");
        integralMapper.updateIntegralDisplay(payId);// 修改成可以查询到积分
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        JSONObject data = (JSONObject) context.getActionContext("integralDto");
        IntegralDto integralDto = data.toJavaObject(IntegralDto.class);
        log.info("");
        Long payId = integralDto.getPayId();
        // 删除该积分
        integralMapper.deletePayId(payId);
        return true;
    }
}

tcc模式注意事项

1.幂等性问题

幂等控制,网络原因,或者重试操作都有可能导致这几个操作的重复执行

2.空回滚

TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;,核心思想就是 回滚请求处理时,如果对应的具体业务数据为空,则返回成功

当然这种问题也可以通过中间件层面来实现,如,在第一阶段try()执行完后,向一张事务表中插入一条数据(包含事务id,分支id),cancle()执行时,判断如果没有事务记录则直接返回,但是现在还不支持

3.防悬挂

事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;

用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求;

跨语言的调用接口如何解决分布式事务问题

不同语言之间需要保证数据的一致性问题,可以采用类似于支付宝异步回调的形式。

你可能感兴趣的:(springboot,每特教育第十期,分布式,阿里云,腾讯云)