目录
分布式事务
产生原因
在一个整套业务中,一个业务的失败回滚对其他业务不可见(子事务不一致
)
CAP定理
- 一致性:用户访问分布式系统中的任意节点,得到的数据必须一致;(及时同步)
- 可用性:用户访问集群中的任意健康节点,必须得到响应,而不是超时或拒绝
- 分区容错性:多个节点形成独立的分区,一个节点挂掉,整个系统也必须能够对外提供服务
在分布式中,分区容错必须实现,所有只能满足一致性和可用性的一种
BASE理论
是对CAP理论的解决思路,包含三个思想
- 基本可用:分布式系统出现故障时,允许损失部分可用,保证核心可用
- 软状态:允许出现中间状态,比如临时不一致、临时可用
- 最终一致性: 最终达到数据一致
分布式事务解决思路
- CP模式:各个子事务执行后相互等待,同时提交或回滚,达成强一致性。但是在事务等待过程中处于弱可用状态
- AP模式:各个子事务分别执行提交,允许结果出现不一致,最后采用弥补的措施恢复数据(多退少补),实现最终一致
2pc&3pc
2pc
二阶段提交 分准备和提交两个阶段;是一种强一致性设计;
- 准备阶段协调者会给各参与者发送准备命令,参与者会执行完sql但不提交;
- 第二阶段检查各事务状态,决定提交或回滚并通知各参与者;
特点
- 阻塞资源:占用数据库资源,性能低,可用性低
- 单点故障:协调者出错,事务失败
- 数据不一致:二阶段出错,通知提交或回滚时断网
3pc
分 准备、预提交和提交三个阶段
- 准备:协调者只询问参与者是否可以提交事务,如果所有参与者都反馈yes 才能进入下一个阶段;这一个阶段时不锁表
- 预提交:这个阶段各分支事务执行完自己的操作,但不提交
- 提交:提交或回滚
特点
- 降低了参与者同步阻塞范围,但是又引入了数据不一致性问题(若出现网络分区)、单点问题依然存在;
Seata
致力于提供高性能和易于使用的分布式事务服务,提供一站式分布式解决方案
Seata架构
- 事务管理器 TM:定义全局事务的范围;开始全局事务;提交或回滚全局事务
- 事务协调者 TC:维持全局和分支事务的状态;协调全局事务的提交、回滚
- 资源管理器 RM:管理分支事务处理的资源;与事务协调器交谈以及注册分支事务和报告分支事务的状态,并驱动分支事务的提交或回滚
四种方案
Seata提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲一定的可用性,无业务入侵
- AT模式:最终一致的分阶段事务模式,无业务入侵,Seata默认模式
- TCC模式:最终一致的分阶段事务模式,有业务入侵
- SAGA模式:长事务模式,有业务入侵
部署TC服务器
https://seata.io/zh-cn/blog/download.html
下载seata-server-1.4.2.zip
- 更改
registry.conf
配置:将配置和注册中心都改为nacosregistry { # 注册中心类型 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { application = "seata-tc-server" serverAddr = "127.0.0.1:8848" group = "DEFAULT_GROUP" namespace = "" cluster = "default" username = "nacos" password = "nacos" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }
- 在nacos添加配置
seataServer.properties
- 在seata数据库创建表
seata-tc-server.sql
cd Program Files\seata\seata-server-1.4.2\bin
启动TC服务seata-server.bat
微服务集成Seata
- 引入依赖
<!--seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <artifactId>seata-spring-boot-starter</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency>
- 更改配置
seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: DEFAULT_GROUP application: seata-tc-server username: nacos password: nacos tx-service-group: seata-demo # 事务组名称 # service: # vgroup-mapping: # 事务组与cluster的映射关系 # seata-demo: SH data-source-proxy-mode: AT config: type: nacos nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos group: SEATA_GROUP data-id: client.properties
XA模式
资源管理器RM(数据库事务)把自己的事务告诉事务协调者TC,随后都报告事务的状态,TC在得知每个事务的状态后决定统一提交或者回滚
XA规范
定义事务协调者和数据库之间的接口规范;事务协调者通过它来通知数据库事务的开始、结束、提交、回滚
XA接口函数由数据库厂商提供
Seata的XA模式
特点
- 强一致性
- 占用数据库锁,可用性降低
- 需要数据库支持事务
实现
AT模式
解决XA资源锁定时间过长的缺陷
- RM向TC注册分支事务
- 执行SQL并提交,记录更改后的快照(undo log)
- 向TC报告事务的状态
- TC检查各个分支事务状态
- TC告诉RM提交或回滚。提交:删除log;回滚:恢复log数据并删除
XA与AT区别
- XA一阶段不提交事务,锁定资源;AT提交事务,不锁资源
- XA依赖数据库回滚;AT依赖日志记录快照实现数据回滚
- XA强一致;AT最终一致
AT模式脏写问题
多线程中,一个事务A保存快照之后改变数据且提交,该事务数据库锁释放,另一事务B在此对这条数据进行修改且提交,TC告诉A事务应该回滚,B就发生脏写
对此引入全局锁,由TC去记录正在操作的数据库某行数据。
- 在A事务执行完SQL获取全局锁,TC就记录了这行数据,然后A事务提交,释放数据库锁;
- B事务就可以获取数据库锁,执行完SQL要提交的时候获取不到全局锁,就一直重试去拿全局锁;
- 如果A事务回滚就要重新拿数据库锁,但是数据库锁真正被B事务持有,形成死锁;所以事务B在默认重试30次后回滚释放数据库锁;
又出现了问题就是,如果有一个Seata没有管理的事务,这个事务就不用获取全局锁,还是会出现问题;
于是A事务就在执行SQL前后分别保存快照,回滚时对比提交后事务是否被改过;
特点
- 锁粒度小,性能好
- 全局锁,读写隔离
- 没有侵入代码
- 软状态,最终一致
实现
TCC模式
原理
与AT一样第一阶段提交事务,但TCC是靠人工编码实现数据恢复
- Try:资源的检测和预留
- Confirm:完成业务操作;要求Try成功Confirm一定成功
- Cancel:预留资源的释放(Try的反向操作)
- 第一阶段与AT一样,分支事务执行(Try)提交之后报告事务状态
- 第二阶段TC检查完各分支事务状态后告诉各分支是提交还是回滚;提交:分支事务执行Confirm;回滚执行Cancel
特点
- 没有快照和全局锁,性能最好
- 不依赖数据库事务,而是依赖补偿操作
- 要多写代码
- 软状态,最终一致
- 要考虑到Confirm和Cancel失败的情况,做好幂等处理
空回滚和业务悬挂
- 当某个分支事务的Try阶段业务阻塞,可能导致全局事务超时而集体回滚;
- 但是这个事务没有执行Try,就多了一次Cancel操作,这就是空回滚;
- 于是就要在执行Cancel时判断这个事务是否执行了Try;
- 而对于已经空回滚的事务,之后又突然执行了Try,但是整体事务已经结束,这个事务就多执行了一次Try,这就是业务悬挂;
- 于是就要在执行Try是判断是否已经回滚过了
需要在数据库里记录当前事务的状态
实现
Saga模式
MQ解决分布式事务
转账:如果减余额和加余额两个操作需要同时执行,那么就等减余额成功后发送消息给加余额服务,这样保证MQ的可靠性就能保证事务(发布确认+持久化+手动应答)
如果下游服务要回滚(业务问题),那只能把这个问题的判断交给上游(项目超卖)
????
消费者在执行完事务后才根据提交还是回滚来决定是否应答该消息;所以mq要设置手动应答
- 上游服务向MQ发送一个半消息
- 然后执行本地事务
- 根据本地事务的执行结果向MQ发送commit或callback
- 如果是callback MQ就丢弃该消息,如果是commit就发送给下游
下游事务执行要等待上游服务执行完成并提交