分布式事务
一、分布式事务基础
1.1 什么是事务?
事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作
都成功,要么所有的操作都被撤销。简单地说,事务提供一种“要么什么都不做,要么做全套”机制
1.2 本地事物
本地事物其实可以认为是数据库提供的事务机制。说到数据库事务就不得不说,数据库事务中的四
大特性:
-
A:原子性(Atomicity),一个事务中的所有操作,要么全部完成,要么全部不完成
-
C:一致性(Consistency),在一个事务执行之前和执行之后数据库都必须处于一致性状态
-
I:隔离性(Isolation),在并发环境中,当不同的事务同时操作相同的数据时,事务之间互不影响
-
D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须永久的保存下来
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单
元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚
1.3 分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布
式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同
的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
1.4 分布式事务的场景
-
单体系统访问多个数据库
一个服务需要调用多个数据库实例完成数据的增删改操作
-
多个微服务访问同一个数据库
多个服务需要调用一个数据库实例完成数据的增删改操作
-
多个微服务访问多个数据库
多个服务需要调用一个数据库实例完成数据的增删改操作
二、分布式事务特性
2.1 CAP定理
CAP定理是分布式系统中的重要理论,在一个分布式系统中最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项,不能同时满足这三项。
-
分区容错性(Partition tolerance)指"the system continues to operate despite arbitrary message loss or failure of part of the system"
即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
-
一致性(Consistency)指"all nodes see the same data at the same time"
即更新操作成功后,所有节点在同一时间的数据完全一致。
-
可用性(Availability)指"reads and writes always succeed"
即服务一直可用且能够正常响应(不保证返回的是最新写入的数据)。
2.2 CAP选择
无数实践证明,CAP只存在理想的状态,真实的的分布式系统必须在CAP,三者中选其二
-
CA:放弃P,放弃分区容错性的话,则放弃了分布式,放弃了系统的可扩展性
-
CP:放弃A,放弃可用性的话,则在遇到网络分区或其他故障时,受影响的服务需要等待一定的时间,再此期间无法对外提供政策的服务,即不可用
-
AP:放弃C,放弃一致性的话,则系统无法保证数据保持实时的一致性,在数据达到最终一致性时,有个时间窗口,在时间窗口内,数据是不一致的
注意:对于分布式系统来说,P是不能放弃的,因此架构师通常是在可用性和一致性之间权衡。
2.3 CAP无法共存证明
理想操作:
用户第一次访问,通过Nginx后,走网段1,执行数据添加,数据同步到网段2的数据库中
用户第二次访问,通过Nginx后,走网段2,执行数据查询,查询到第一次添加的data数据
非理想状态
用户第一次访问,通过Nginx后,走网段1,执行数据添加,数据无法同步到网段2的数据库
用户第二次访问,通过Nginx后,走网段2,执行数据查询
分布式系统存在意义,最低要求必须保证分区容错性成立,那么就意味着,上面操作存在2种选择。
选择可用性(A),牺牲一致性©:返回空数据,
选择一致性©,牺牲可用性(A):阻塞等待,直到网段1,网段2连通,数据同步完成之后,再返回data数据。
2.4 BASE理论
BASE是Basically Available(基本可用)、**Soft state(软状态)和Eventually consistent(最终一致性)**三个短语的简写。
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性。
Basically Available(基本可用)
基本可用指分布式系统在出现故障时,系统允许损失部分可用性,即保证核心功能或者当前最重要功能可用。对于用户来说,他们当前最关注的功能或者最常用的功能的可用性将会获得保证,但是其他功能会被削弱
Soft state(软状态)
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually consistent(最终一致性)
上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。
总结
总的来说,BASE 理论面向的是大型高可用可扩展的分布式系统,和传统事务的 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的。
三、分布式事务解决方案
3.1 方案汇总
3.2 方案1:2PC
XA协议
要讲清楚2PC模式,需要科普一个协议:XA协议
XA协议是X/Open的组织定义的分布式事务处理标准规范(DTP)。它定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的通讯接口。在TM与多个RM之间形成一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。目前几乎所有的主流数据库都对XA规范提供了支持。
XA协议包含有几个角色:
- AP(Application Program) : 既应用程序,可以理解为使用DTP分布式事务的程序。
- RM(Resource Manager) : 即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。
- TM(Transaction Manager) : 事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者(TM)的角色来协调管理各参与者(也可称之为各本地资源RM)的提交和回滚,二阶段分别指的是准备和提交两个阶段。
第一阶段:准备阶段
准备阶段,事务协调者™会给各事务参与者(RM)发送准备命令(prepare),参与者准备成功后返回(ready)
- 协调者向所有参与者发送事务操作指令,参与者执行除了事务提交外所有操作
- 如参与者执行成功,给协调者反馈执行成功,否则反馈中止,表示事务失败
第二阶段:提交阶段
协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者commit提交或者rollback回滚
- commit提交
当第一阶段所有参与者都反馈成功时,协调者发起正式提交事务的请求,当所有参与者都回复提交成功时,则意味着完成事务。
- 协调者节点向所有参与者发出正式提交的 commit 请求。
- 收到协调者的 commit 请求后,参与者正式执行事务提交操作,并释放在整个事务期间内占用的资源。
- 参与者完成事务提交后,向协调者节点发送已提交消息。
- 协调者节点收到所有参与者节点反馈的已提交消息后,完成事务。
- Rollback回滚
如果任意一个参与者节点在第一阶段返回的消息为中止(或者异常),或者协调者节点在第一阶段的询问超时,无法获取到全部参数者反馈,那么这个事务将会被回滚。
- 协调者向所有参与者发出 rollback 回滚操作的请求
- 参与者执行事务回滚,并释放在整个事务期间内占用的资源
- 参与者在完成事务回滚之后,向协调者发送回滚完成的反馈消息
- 协调者收到所有参与者反馈的消息后,取消事务
缺点
- 性能问题:执行过程中,所有参与节点都是事务阻塞性的,当参与者占有公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态,为了数据的一致性而牺牲了可用性,对性能影响较大,不适合高并发高性能场景
- 可靠性问题:2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会都处于锁定事务资源的状态中,而无法继续完成事务操作
- 数据一致性问题:在阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
- 二阶段无法解决的问题:协调者在发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了,那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
优点
- 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。
3.3 方案2:3PC
概念
3PC,三阶段提交协议,是二阶段提交协议的改进版本,以解决2PC存在的缺陷问题, 具体改进如下:
-
在协调者和参与者中都引入超时机制
-
引入确认机制,当所有参与者能正常工作才执行事务
所以3PC分为3个阶段:CanCommit 准备阶段、PreCommit 预提交阶段、DoCommit 提交阶段。
第一阶段:CanCommit 准备阶段
- 协调者向参与者发送 canCommit 请求,参与者如果可以提交就返回Yes响应,否则返回No响应,具体流程如下:
- 事务询问:协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
- 响应反馈:参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
第二阶段:PreCommit 阶段
协调者根据参与者的反应情况来决定是否可以进行事务的 PreCommit 操作。根据响应情况,有以下两种可能:
执行事务:返回都是yes
所有参与者向协调者发送了Yes响应,将会执行执行事务
- 协调者向参与者发送 PreCommit 请求,并进入准备阶段
- 参与者接收到 PreCommit 请求后,会执行本地事务操作,但不提交事务
- 参与者成功的执行了事务操作后,返回ACK响应,同时开始等待最终指令。
中断事务:返回存在no
如果存在一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断
- 协调者向所有参与者发送 abort 请求。
- 参与者收到来自协调者的 abort 请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
第三阶段:doCommit阶段
该阶段进行真正的事务提交,也存在2种情况:
提交事务:返回都是yes
第二阶段的preCommit 请求,所有参与者向协调者发送了Yes响应,将会提交事务
- 协调接收到所有参与者发送的ACK响应,那么他将从预提交状态进入到提交状态,并向所有参与者发送 doCommit 请求
- 参与者接收到doCommit请求之后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源
- 事务提交完之后,参与者向协调者发送ack响应。
- 协调者接收到所有参与者的ack响应之后,完成事务。
中断事务:返回存在no
如果存在一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断
- 协调者处向所有参与者发出 abort 请求
- 参与者接收到abort请求之后,马上回滚事务,释放所有的事务资源。
- 参与者完成事务回滚之后,向协调者反馈ACK消息
- 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
缺点
数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
优点
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
3.4 方案3:TCC
概念
TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
怎么理解?看下图
一个完整的 TCC 业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC 模式要求从服务提供三个接口:Try、Confirm、Cancel。
Try(尝试)
这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
Confirm(确认)
确认执行业务操作,不做任何业务检查, 只使用Try阶段预留的业务资源。通常情况下,采用TCC
则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的
出错了,需引入重试机制或人工处理。
Cancel(取消)
取消Try阶段预留的业务资源。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若
Cancel阶段真的出错了,需引入重试机制或人工处理。
注意要点:
并且由于 confirm 或者 cancel 有可能会重试,因此对应的部分需要支持幂等。
经典案例
需求:用户下单,扣库存,扣款
假设: 库存总数10,购买2,账户余额1000,扣款200
优点
- 性能提升:具体业务来实现,控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群
缺点
TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
3.5 方案4: 可靠消息服务
基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。假设有A和B两个
系统,分别可以处理任务A和任务B。此时存在一个业务流程,需要将任务A和任务B在同一个事务中处
理。就可以使用消息中间件来实现这种分布式事务。
RocketMQ事务消息流程图
1)事务消息发送及提交
(1) 发送消息(half消息)
(2) 服务端响应消息写入结果
(3) 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)
(4) 根据本地事务状态执行Commit或者Rollback(Commit操作生产消息索引,消息对消费者可见)
2) 事务补偿
(1) 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
(2) Producer收到回查消息,检查回查消息对于的本地事务的状态
(3) 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用户解决消息Commit或者Rollback发生超时或者失效的情况
3) 事务消息状态
事务消息共有三种状态,提交状态,回查状态,中间状态:
- TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息
- TransactionStatus.RollbackTransaction: 回滚事务,它代表消息将被删除,不允许被消费
- TransactionStatus.Unknown: 中间状态,它代表需要消息队列来确认状态
优点
- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合
缺点
- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
- 业务处理服务需要实现消息状态回查接口。
四、分布式事务解决方案
4.1 RocketMQ事务消息实现
需求: 用户订单退款(1.积分服务进行积分增加 2.订单服务进行状态修改)
步骤1: 解压RocketMQ案例代码.rar
,导入到IDEA中.
步骤2: 添加事务监听类
package cn.wolfcode.mq;
import cn.wolfcode.domain.OrderInfo;
import cn.wolfcode.mapper.OrderInfoMapper;
import cn.wolfcode.service.IOrderInfoService;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* Created by wolfcode-lanxw
*/
@Component
@RocketMQTransactionListener
public class OrderTXMsgListener implements RocketMQLocalTransactionListener {
@Autowired
private IOrderInfoService orderInfoService;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println("执行本地事务方法");
String orderNo = (String) arg;
try{
orderInfoService.changeRefundStatus(orderNo,OrderInfo.STATUS_REFUND);
return RocketMQLocalTransactionState.UNKNOWN;
}catch(Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
System.out.println("执行本地事务回查方法");
String orderNo = (String) msg.getHeaders().get("orderNo");
OrderInfo orderInfo = orderInfoService.find(orderNo);
if(OrderInfo.STATUS_REFUND.equals(orderInfo.getStatus())){
return RocketMQLocalTransactionState.COMMIT;
}else{
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
步骤3:发送事务消息
@Override
public String refund(String orderNo) {
OrderInfo orderInfo = orderInfoMapper.find(orderNo);
OperateIntergralVo vo = new OperateIntergralVo();
vo.setPhone(orderInfo.getPhone());
vo.setValue(orderInfo.getIntegral());
Message<OperateIntergralVo> message = MessageBuilder.withPayload(vo).setHeader("orderNo",orderNo).build();
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction("tx_topic", message, orderNo);
if(LocalTransactionState.COMMIT_MESSAGE.equals(result.getLocalTransactionState())){
return "退款成功";
}else if(LocalTransactionState.ROLLBACK_MESSAGE.equals(result.getLocalTransactionState())){
return"退款失败";
}else{
return"未知状态";
}
}
4.2 分布式事务框架Seata
4.2.1 Seata简介
官网:http://seata.io/zh-cn/
源码:https://github.com/seata/seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
XA工作原理
AT模式工作原理
TCC工作流程
4.2.2 Seata部署
-
下载
地址: https://github.com/seata/seata/releases/tag/v1.7.0
-
上传,将
seata-server-1.7.0.zip
上传到/usr/local/software
目录下unzip /usr/local/software/seata-server-1.7.0.zip -d /usr/local
-
打开
conf/application.yml
进行修改,内容修改如下:server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata extend: logstash-appender: destination: 127.0.0.1:4560 kafka-appender: bootstrap-servers: 127.0.0.1:9092 topic: logback_to_logstash console: user: username: seata password: seata seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP data-id: seataServer.properties registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP cluster: default security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login
-
执行
seata-server-1.7.0/seata/script/server/db
目录下的mysql.sql脚本,执行之后的结果如下: -
在Nacos中添加seata的配置文件,文件命名为: seataServer.properties
注意需要修改成你的数据库地址/账号/密码
#Transaction storage configuration, only for the server. store.mode=db store.lock.mode=db store.session.mode=db #These configurations are required if the `store mode` is `db`. store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql:///seata?useSSL=false&useUnicode=true&rewriteBatchedStatements=true store.db.user=root store.db.password=admin store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.distributedLockTable=distributed_lock store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000
-
启动seata-server
/usr/local/seata/bin/seata-server.sh -h 192.168.202.200
-
访问测试 http://192.168.202.129:7091
默认密码: seata/seata
4.2.3 Seata客户端集成
1.在项目中添加依赖
<!-- 注意一定要引入对版本,要引入spring-cloud版本seata,而不是springboot版本的seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!-- 排除掉springcloud默认的seata版本,以免版本不一致出现问题-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
2.在配置文件中添加如下配置
seata:
enabled: true
application-id: seata_tx
tx-service-group: seata_tx_group
service:
vgroup-mapping:
seata_tx_group: default
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
data-source-proxy-mode: XA
4.2.4 XA模式
使用: 在业务方法中贴@GlobalTransational即可
工作原理
-
优点:
- 事务强一致性,满足ACID原则 - 常用的数据库都支持,实现简单,并且没有代码侵入
-
缺点:
- 第一阶段锁定数据库资源,等待二阶段结束才释放,锁定资源过长,性能较差 - 依赖关系型数据库的实现事务
4.2.5 AT模式
使用:
1.在业务方法中贴@GlobalTransational
2.修改配置文件中的代理模式,设置为AT模式
seata:
data-source-proxy-mode: AT
3.在每个数据库中增加undo_log
表.
在官网中拷贝数据库脚本
https://seata.io/zh-cn/docs/dev/mode/at-mode.html
-- 注意此处0.7.0+ 增加字段 context
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
工作原理
XA VS AT
- XA模式一阶段不提交事务,锁定资源; AT模式一阶段直接提交,不锁定资源(会有全局锁)
- XA模式依赖数据库实现回滚; AT利用数据快照实现数据回滚
- XA模式强一致性;AT模式最终一致(一阶段提交,此时有事务查询,就存在不一致)
利用全局锁的机制来实现事务间的读写隔离
https://seata.io/zh-cn/docs/dev/mode/at-mode.html
-
优点:
- 一阶段完成直接提交事务,释放资源,性能较好 - 利用全局锁实现读写隔离 - 没有代码侵入,框架自动完成回滚与提交
-
缺点:
- 框架的快照功能影响性能,但比XA模式要好很多 - 两阶段之间存在数据不一致情况,只能保证最终一致
4.2.6 TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的TCC通过人工编码来实现数据恢复。
- Try:资源的检测和预留
- Confirm:完成资源操作业务;要求Try 成功,Confirm 一定要能成功
- Cancel:预留资源释放,可以理解为try的反向操作
工作原理:
TCC的异常问题
TCC模式中,在执行Try,执行Confirm,执行Cancel 过程中会出现意外情况,导致TCC模式经典问题:空回滚,业务悬挂,重试幂等问题。
-
空回滚
当某个分支事务try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作,RM在没有执行try操作就执行cancel操作,此时cancel无数据回滚,这就是空回滚。
-
业务悬挂
当发生的空回滚之后,当阻塞的Try正常了,RM先执行空回滚(cancel)后,又收到Try操作指令,执行业务操作,并冻结资源。但是事务已经结束,不会再有confirm 或cancel了,那刚执行try操作冻结资源,就被悬挂起来了。这就是业务**悬挂**
-
重试幂等
因为网络抖动等原因,TC下达的Confirm/Cancel 指令可能出现延时,发送失败等问题,此时TC会启用重试机制,到时,RM可能收到多个confirm或cancel指令,这就要求confirm接口或者cancel接口,需要能够保证幂等性。
在seata1.5.1版本之前,我们需要手动在业务代码中去解决空回滚,业务悬挂,重试幂等这些问题.在seata1.5.1版本之后,我们可以通过配置的方式解决这些问题(默认是没有开启的,需要配置)
使用步骤:
1.新增事务日志控制表
脚本: https://github.com/seata/seata/blob/v1.5.1/script/client/tcc/db/mysql.sql
每个业务数据库都新增这个表
-------------------------- The script use tcc fence ------------------------
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
2.增加业务接口
@LocalTCC
public interface IUsableIntegralServiceTCC {
@TwoPhaseBusinessAction(name = "substractIntegralTry",
commitMethod = "substractIntegralConfirm",
rollbackMethod = "substractIntegralCancel",
useTCCFence = true)
Boolean substractIntegralTry(@BusinessActionContextParameter(paramName = "vo")OperateIntegralVo vo);
void substractIntegralConfirm(BusinessActionContext ctx);
void substractIntegralCancel(BusinessActionContext ctx);
}
3.实现接口
@Service
public class UsableIntegralServiceImplTCC implements IUsableIntegralServiceTCC {
@Autowired
private UsableIntegralMapper usableIntegralMapper;
@Override
public Boolean substractIntegralTry(OperateIntegralVo vo) {
System.out.println("Try方法");
int effectCount = usableIntegralMapper.freezeIntegral(vo.getPhone(), vo.getValue());
return effectCount>0;
}
@Override
public void substractIntegralConfirm(BusinessActionContext ctx) {
System.out.println("Confirm方法");
System.out.println(ctx.getActionContext("vo"));
}
@Override
public void substractIntegralCancel(BusinessActionContext ctx) {
System.out.println("Cancel方法");
System.out.println(ctx.getActionContext("vo"));
}
}