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
1.redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
2.undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
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日志表
1.事务分组:seata的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
2.集群:seata-server服务端一个或多个节点组成的集群cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与Seata服务端集群的映射关系。
https://seata.io/zh-cn/docs/user/txgroup/transaction-group-and-ha.html
其中,projectA所有微服务的事务分组tx-service-group设置为:projectA,projectA正常情况下使用guangzhou的TC集群(主)
那么正常情况下,client端的配置如下所示:
seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou
假如此时guangzhou集群分组整个down掉,或者因为网络原因projectA暂时无法与Guangzhou机房通讯,那么我们将配置中心中的Guangzhou集群分组改为Shanghai,如下:
seata.service.vgroup-mapping.projectA=Shanghai
并推送到各个微服务,便完成了对整个projectA项目的TC集群动态切换。
目前授课版本Seata1.4.2 版本 https://github.com/seata/seata/releases/tag/v1.4.2
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;
创建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
registry.conf 为nacos
group = “SEATA_GROUP” 连接 nacos 分组:SEATA_GROUP 而
我们nacos 默认的分组名称:DEFAULT_GROUP
注意事项:以后新增SEATA 相关配置文件内容,注意 一定要是
SEATA_GROUP 分组,否则读取不到该配置文件内容。
application = “seata-server” 注册nacos 服务名称
修改配置中心地址
1.将seata config.txt 配置上传到nacos中,在nacos中创建一个新的命名空间为 mayikt-seata
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 目录中
文件内容:(主要修改成自己的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
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项目
6.配置文件上传到nacos中成功
\7. 登录nacos 平台 选择 mayikt-seata 命名空间 配置文件上传成功
D:\path\cloud\seata-server-1.4.2\seata\seata-server-1.4.2\bin
seata-server.bat 双击启动即可
查看nacos 服务管理
客户端整合过程中,如果启动发生错误 大多数原因是由于 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
同时在 nacos -config 配置:
service.vgroupMapping.mayikt-cloud:default
注意分组是:SEATA_GROUP
registry.conf 配置文件:
cluster = “default”
否则会报错:
参考seata github: https://github.com/seata/seata/issues/2406
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
在接口上加上@GlobalTransactional,启动项目
会显示 rm服务注册成功。
查询nacos 服务注册中心
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';
@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();
}
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());
}
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参与方 使用拦截器 接受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;
}
\1. 如果发起方没有报错的情况下, 发起方通知给tc事务协调者,tc事务协调者在通知给参与方,删除该undolog日志。
2.如果发起方失败的情况下,则会查询本地数据库中的undolog日志,根据undolog日志逆向实现回滚数据DataSourceManager# branchRollback方法中。
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 = ?
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是否有主键(没有主键会报错)
支付调用完积分服务 积分增加成功
插入前置镜像—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日志 已经存在
解决办法:
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"
}
]
]
}
]
]
}
}
]
]
}
at模式----
tcc —
2pc 两阶段
第一阶段 —准备阶段
第二阶段 —提交或者回滚阶段
tcc 需要自己编写代码的形式 提交和回滚。
at模式 开发者不需要写任何代码的
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
一阶段 prepare 行为
二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
tcc 框架:1.tcc-transaction 2. seata 支持TCC 模式 3.bytetcc 等。
1.两阶段提交协议可以理解为2pc,也就是分为参与者和协调者,协调者会通过两次阶段实现数据最终的一致性的。
2.2PC和3pc的区别就是解决参与者超时的问题和多加了一层询问,保证数据的传输可靠性。
两段提交顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段) ; 第二阶段,提交阶段(执行阶段),中间必须要有一个协调者。
第一节阶段:
协调者会给每个参与者发送一封邮件,如果所有的参与者回复 ok,
如果中间有一个参与者 回复fail。
第二阶段:
就会给每个参与者发送一封确认的邮件,所有参与者发送fail。
例如在我们团队中有 小军、小安、小薇 三个人,打算明天想去聚餐?那如何达成一致呢?
我们就必须选出一名 组长 假设选举 小军为组长 小安和小薇就是为 组员
第一阶段:
小军在给小安和小薇 发一封邮件说“发送邮件:明天晚上7点准时聚餐,你们有时间吗?”
小安和小薇 如果回复“ok”
第二阶段:
小军收到了 小安哥小薇回复的“ok”,就会给 小安和小薇在发一封邮件 说明天晚上7点准时聚餐
小军收到了 小安回复的"ok",小薇回复的“fail”,就会给 小安和小薇在发一封邮件 说明天晚上7点不能够聚餐
小薇没有时间。
我们协调者,会给每一个参与者发送Prepare(预备)消息,执行本地数据脚本但不提交事务;
如果协调者收到了参与者成功的消息,则会给每个参与者发送提交(Commit)消息 , 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,显然2PC做到了所有操作要么全部成功、要么全部失败。
如果协调者收到了参与者成功的消息,则会给每个参与者发送提交(Commit)消息 , 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,显然2PC做到了所有操作要么全部成功、要么全部失败。
1、协调者节点挂了此场景比较主流的方案是选举机制,首次即通过选举决定谁是协调者,当协调者挂掉后重新选举。
2、commit阶段出现部分失败
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 | 取消执行业务操作,实际回滚数据,需保证幂等 |
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;
}
}
1.幂等性问题
幂等控制,网络原因,或者重试操作都有可能导致这几个操作的重复执行
2.空回滚
TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;,核心思想就是 回滚请求处理时,如果对应的具体业务数据为空,则返回成功
当然这种问题也可以通过中间件层面来实现,如,在第一阶段try()执行完后,向一张事务表中插入一条数据(包含事务id,分支id),cancle()执行时,判断如果没有事务记录则直接返回,但是现在还不支持
3.防悬挂
事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;
用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求;
不同语言之间需要保证数据的一致性问题,可以采用类似于支付宝异步回调的形式。