分布式事务-SpringBoot集成Seata
1.本地事务和分布式事务概念
事务四大特性
- 原子性:事务不可再分
- 一致性:数据改变前后,总量必须一致
- 隔离性:事务之间相互隔离,互不干扰
- 持久性:事务一旦提交,数据就会持久化到磁盘,不能回滚
本地事务:所有的事务都是基于数据库的,那么我们在代码中加@Transactional其实是使用的数据库的Begin/Commit/Rollback,底层使用的是动态代理。本地事务只能管理到当前服务连接的那个数据库的事务,如果一个事务中操作了多个数据库在回顾的时候只能回滚当前服务连接的那个数据库,没办法回滚其它数据库
分布式事务:用于解决在一个方法中操作多个数据库的事务,分布式事务其实就是将多个数据库加入到同一组事务中,当某一个数据库发生异常时整体回滚
2.分布式事务的解决方案
2.1 XA协议
- 简单理解就是XA规范协议是一种事务协议,通过这种协议来通知数据库事务的开始、结束、提交、以及回滚,XA 接口函数由数据库厂商提供。XA协议使用二阶段提交来处理分布式事务,说的更明白一点就是XA协议保证了分布式事务的原子性,要么都成功,要么都失败。
- XA协议采用两阶段提交方式来管理分布式事务
2.2 2PC模式
2PC就是基于XA协议进行实现的,采用两阶段提交 2PC(Two Phase Commitment Protocol)来管理分布式事务,所谓二阶段是有两个阶段组成,一阶段投票阶段和二阶段提交阶段。同时它是由“事务协调器”和若干“事务执行者”两个角色组成。
第一阶段:准备阶段( 投票阶段 )
- 事务协调器向所有事务参与者发请求,询问是否可以执行提交操作(你们都可以执行事务操作吗?),并开始等待各参与者节点的响应。
- 事务参与者收到协调者的指令开始执行事务操作但是不会提交事务,同时写Undo log(写操作之前首先将数据备份log,如果要回滚就从这个log进行数据还原) 和 Redo log(修改数据在buffer pool缓冲池中修改,Redo log是对这个缓冲池的内容做持久,避免修改的数据丢失) 。
- 如果参与者事务操作都执行成功(注意哦,没提交事务哦),那么就会回复 事务协调器 “准备OK” ,如果事务操作失败,那么就会回复执行者“准备不OK”。
第二阶段:提交阶段
正常流程
- 事务协调器会收到参与者的回复,如果所有的参与者都回复“准备ok”,意味着所有的参与者都可以完成事务操作,那么事务协调器会向每个事务参与者发送一个“commit” 提交事务指令(既然大家都可以进行事务操作,那大家都提交事务把)
- 事务参与者收到指令就开始提交事务,然后会向事务协调器回复“完成”,事务协调器收到所有参与者都回复完成,事务完成
回滚流程
- 如果再第一阶段事务协调器收到了某个事务参与者回复“准备不ok”即事务操作执行失败,那么事务协调器会向所有的事务参与者发送“rollback”回滚执行(有一个成员不ok,那大家都散了吧,今天的事情搞不成了)
- 事务参与者收到指令,回滚之前的事务操作,即:将数据还原到“Undo log”的数据,然后向事务协调者回复“回滚成功”,当事务协调器收到所有的参与者回复“回滚成功”后,取消事务。
发送回滚指令
二阶段提交的问题
- 二阶段能保证分布式事务的原子性,但是也有一些明显的缺陷。比如:在第一阶段,如果参与者迟迟不回复协调者,就会造成事务的阻塞,性能不好。
- 单节点故障,如果协调器挂了,参与者会阻塞,比如在第二阶段,如果事务协调器宕机,参与者没办法回复信息,长时间处于事务资源锁定,造成阻塞(事务操作是要加锁的)。
- 在第二阶段,如果在事务协调器发出"commit"执行后宕机,一部和参与者收到了消息提交了事务,而一部分没有消息没法做出事务提交操作,这样就出现了数据不一致。
- 在第二阶段,如果事务事务协调器发出“commit”指令后宕机,收到“commmit”指令的参与者也宕机了,那么事务最终变成了什么效果,提交了还是没提交?没有谁知道。
2.3 3PC模式
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本,
3PC在2PC的功能上做了两个改动,一是在协调者和事务参与者之间引入了超时机制,在第一阶段和第二阶段中插入一个准备阶段 , 保证了在最后提交阶段之前各参与节点的状态是一致的,那么现在的三阶段分为了:
- canCommit“询问是否能提交,
- papreCommit”准备提交阶段 ,
- doCommit”提交阶段。
第一阶段“canCommit” :
- 事务协调者向事务参与者发送 canCommit 请求询问是否能提交事务,然后等待所有事务参与者的返回
- 事务参与者接收到事务些调整的canCommit指令,然后自身认为能够提交事务则返回 “yes”否则返回“no”
第二阶段“papreCommit”
事务协调者收到所有的事务参与者的canCmmit指令的反馈结果,这里有两种情况,一是所有的反馈都是yes,二是有部分的事 务参与者返回No,后者反馈超时。
正常流程
- 如果事务协调者收到所有的事务参与者的canCmmit指令反馈结果都为YES,那么就进入papreCommit阶段。事务协调者向事务参与者发送 “papreCommit”指令。
- 事务参与者收到“papreCommit”指令,开始进行事务操作,并将undo和redo信息记录到事务日志中,如果顺利执行事务操作,则反馈ACK确认信息,然后等待下一步指令。
中断事务
- 如果事务协调者收到所有的事务参与者的canCmmit指令反馈结果出现了NO,或者等待超时,那么就执行事务中断,向所有的事务参与者发送“abort”中断指令。
- 事务参与者接收到“abort”指令,中断事务,当然如果事务参与者迟迟未收到事务协调者的指令等待超时也会中断事务。
第三阶段“doCommit阶段”
这里准备提交事务了,这里有两种情况,如果事务协调者收到所有的事务参与者的papreCommit指令反馈结果都是ACK,那么进入doCommit阶段,否则会中断事务。
正常流程
- 事务协调者收到所有的事务参与者的papreCommit指令反馈结果都是ACK,然后向事务参与者发送“doCommit”指令,通知提交事务。
- 事务参与者收到“doCommit”指令,正式执行事务提交,并且释放所有事务资源,返回向事务协调者返回事务结果状态“ACK”完成
- 事务协调者收到所有的事务参与者都返回ACK成功,完成事务。
中断事务
- 事务协调者收到的事务参与者的papreCommit指令反馈结果有的不是ACK,那么事务协调者然后向事务参与者发送“abort”事务中断指密令。
- 事务参与者收到“abort”事务指令,会根据unlog日志文件还原数据,然后释放事务资源,然后向事务协调者发送回滚“ACK”消息
- 事务协调者收到所有的事务参与者都返回ACK消息,取消事务。
2.4 TCC事物补偿
TCC(Try Confirm Cancel) 事务补偿机制,即每一个操作都要做相应的补偿机制,即如何确认操作成功,如果操作失败如何撤销事务。它分为三个阶段
- Try : try的意思是尝试,其实这个步骤是用来做业务的预处理,可以理解为是做一些准备工作,等到Confirm之后这些操作才算成功。
- Confirm :确认,如果所有的事务参与者都try成功,执行commit对业务做提交操作,或者可以理解成对try的工作作出确认。
- Cancel:取消,如果try失败需要回滚,即取消try的预处理操作
2.5 MQ消息最终一致性
大致流程是:
1.主业务方向发送一个“Prepared”准备消息到MQ,这个消息还未被确认,不会发生给消费者,然后MQ向生产者发送确认收到消息,然后主业务方执行自己的业务,并提交本地事务,成功后向MQ确认之前的“Prepared”消息发生给消费者,如果失败MQ将当前半消息删除,取消事务。
2.消息接受者方收到消息后执行业务逻辑,提交本地事务,然后向MQ返回ACK确认消息,如果ACK消息为成功,MQ则删除当前消息;如果消费者消息接受失败或返回ACK是失败,会进行重试,保证消息最终被消费。直到16次后还是失败,消息会进入死信队列,该消息不会被删除,也不会重发,需要人工介入;
RocketMQ的事务消息:使用MQ的分布式事务是最终一致性
- 生成者发送一个半事务消息给MQ,MQ告诉生产者接收到了,此时生产者方就去执行本地事务
- 如果执行成功那么告诉MQ执行Commit,如果本地事务失败MQ执行Rollback。那么MQ执行Commit就是把此消息发送给消费者,消费者接收到消息之后执行本地事务,如果执行成功那么响应ACK确认成功,如果执行失败那么响应ACK失败,MQ接收到成功删除此消息,如果失败那么会再次发送,直到16次之后还是失败那么进入到死信队列,此时就需要我们人工去检查代码手动发送消息再次消费。如果MQ执行Rollback那么就会把此消息进行删除。
- 如果避免生产者一直不提交commit或rollback还准备了一个回查机制,调用我们写的一个方法,在方法中去检查生产者的本地事务是否执行成功,如果成功提交commit,如果失败提交rollback
2.6 Seata框架
seata(Simple Extensible Autonomous Transaction Architecture) 是 阿里巴巴开源的分布式事务中间件,致力于提供高性能,零入侵和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 的设计思路是将一个分布式事务可以理解成一个全局事务,下面挂了若干个分支事务,而一个分支事务是一个满足 ACID 的本地事务,因此我们可以操作分布式事务像操作本地事务一样。
Seata重要组成
- Transcation ID(XID) : 由事务协调者创建的全局唯一的事务ID
- Transaction Coordinator(TC) - 事务协调器:一个独立运行的组件,负责维护全局事务的运行状态,负责根据TM的指令协调并驱动全局事务的提交或回滚,负责向资源管理器发起事务提交,回滚指令。
- Transaction Manager - 事务管理器 :控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议,通知TC提交或者回滚事务。
- Resource Manager(RM) - 资源管理器: 控制分支事务,负责分支事务注册、负责向TC汇报分支事务状态,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚
正常执行流程:
- 系统启动TM事务管理者以及RM资源管理者需要向事务协调器进行提交注册,可以看做是一种初始化。
- 在Bussiness的业务主方法上我们需要打上@GlobalTransationl注解,通过这个注解,事务管理器TM向事务协调者(TC)申请开启一个全局分布式事务,事务协调者创建全局事务后返回全局唯一的 XID,这个XID 会在涉及微服务的整个全局事务的上下文中进行传播。
- 业务开始,Bussiness通过Feign调用Order,并传递全局事务XID,Order在做写操作的时候,RM资源管理器 (Order集成了RM)向事务 协调器TC 注册本地分支事务,该分支事务归属于拥有相同 XID 的全局事务,同时事务协调者TC会返回一个分支事务ID:“branchId” 。说明一下:Seate通过代理DataSource向TC发起分子事务注册的。
- 这个时候Order会正常的写数据库,然后会写入一个undo log(这个日志文件记录了数据库修改前的数据,用来做回滚),然后提交分支事务(注意,分支事务已经提交了) , 最后向TC上报事务处理状态。当然Account做的事情和Order是相同的。
- Order和Account都调用完成,代码回到Business,这时TM事务管理器向TC事务协调者发起全局事务提交请求,TC向RM事务分支发起事务提交请求,RM(Order, Account)直接删除到undo log日志文件即可,因为之前已经提交了本地事务
异常执行流程:
前面2个步骤都跟上面一样,这里省略 , 在第 3 步的时候,Order在可能因为某种原因本地分支事务提交失败了,那么RM会向TC上报一个失败的事务状态
在第 4 步 ,这个时候代码回到Business,这时TM事务管理器会向TC事务协调者发起全局事务回滚请求,TC向RM事务分支发起事务回滚请求,RM(Order, Account)收到回滚指令,然后会解析undo log,指向反向操作,把数据还原到修改之前,删除undo log,提交本地事务。
3.微服务整合seata
3.1导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3.2yaml配置seata
项目的yaml中对seata做配置
- 这里的tx-service-group: ${spring.application.name}-tx-group要和seataServer.properties中的保持一致
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: ${spring.application.name}-tx-group #it-fccar-service-driver-tx-group
#对应seataServer.properties中的service.vgroupMapping.it-drive-service-driver-tx-group=default
config:
type: nacos
nacos:
server-addr: ${NACOS_HOST:8.137.85.173:8848}:${NACOS_PORT:8848}
username: nacos
password: nacos #账号再nacos管理界面配置
namespace: fccar-dev #取pom.xml中的命名空间
data-id: seataServer.properties
group: DEFAULT_GROUP #重要,和seataServer.properties保持一样
registry:
type: nacos
nacos:
application: service-seata #seata的服务名
server-addr: ${NACOS_HOST:8.137.85.173:8848}:${NACOS_PORT:8848}
username: nacos
password: nacos #账号再nacos管理界面配置
namespace: fccar-dev #取pom.xml中的命名空间
group: DEFAULT_GROUP
enable-auto-data-source-proxy: true #开启seata的datasource自动代理
在启动类贴注解开启seata , 因为yaml做了配置,下面注解可以不要
@EnableAutoDataSourceProxy(dataSourceProxyMode="AT",useJdkProxy=false)
3.3.开启全局事务
开启全局事务,在入口方法贴注解
- 注意:只在业务入口的微服务的方法上贴即可
@GlobalTransactional(name = "driver-wechat-register",rollbackFor = Exception.class)