分布式事务常见解决方案
Posted 热爱编程的大忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式事务常见解决方案相关的知识,希望对你有一定的参考价值。
本文不会对细节展开叙述,因此更多的是提炼总结,遇到不懂的知识点,大家需要自己去查询资料,完整学习一遍。
事务
事务具有ACID特性:
原子性,一致性,隔离性,持久性
四个事务隔离级别:
读未提交: 脏读,不可重复读,幻读
读已提交: 不可重复度,幻读
可重复读: 幻读
可串行化
幻读: 两个并行的事务,事务1读取先读取一个范围的记录,之后事务2在该范围内删除或者新增多条数据,事务1再次查询时,就会少读数据或者读取到新增的数据。
1.采用对读取到的数据加悲观锁的方式,是能够解决可重复读,但是无法避免幻读问题
2.采用mvcc快照机制,那么就可以解决幻读问题
Innodb本地事务
Innodb采用MVCC多版本并发控制实现读写并发执行,并且以此来支持读已提交和可重复读两个隔离级别,核心在于快照创建时间点不同,前者是每次select时创建快照版本,后者是在事务开始时创建快照版本。
Innodb是通过日志和锁来保证事务的ACID特性的,包括上面的MVCC也需要日志的支持,具体如下:
innodb串行化隔离级别采用加锁实现
通过redo log重做日志,保障事务的持久性
通过undo log撤销日志,保证事务的原子性和一致性
undo log日志如何保障事务原子性?
事务中相关修改操作都会被记录到undo log中去,同时为了确保undo log的持久性,也会将undo log日志相关修改记录到redo log中。
每个记录都有一个隐藏的rollback字段,指向undo log日志中上一个版本的记录,需要回滚时,只需要通过rollback指针回滚到上一个版本的状态即可。
并且为了支持支持MVCC特性,多次修改的版本都会保存到undo log日志中,以此形成当前记录的版本链。
事务提交时,会将相关不可重用的undo log加入history链表,通过单独的purge后台线程进行回收,由于mvcc机制,所以必须确定undo log不被任何事务所引用时,才可以被回收。
redo log日志如何保证事务持久性?
事务过程中产生的修改会被记录到redo log中,修改记录完毕后,此时redo log处于prepare状态
接着将修改记录到binlog中,并将修改通过fsync强制落盘
最终标记redo log日志状态为commit,并将此次事务修改通过fsync强制落盘
为了维护数据库的一致性,必须确保binlog和redo log两者写入的原子性,这里通过二阶段提交协议实现。
如何解决幻读?
在每次事务开始时,创建一个一致性快照,以后每次读,都从快照读取
使用间隙锁(gap lock)锁住要查询的范围,或者next-key-lock锁住当前记录和要查询的范围
分布式事务
分布式事务场景有如下几种情形:
跨库事务
分库分表
微服务场景
分布式事务存储端多样性:
分布式事务学习思路
理论依据: CAP理论-->基于CAP延展的BASE理论
落地方案: XA规范(2PC,3PC),TCC方案,SAGA方案,本地消息表,mq事务消息
CAP理论
一致性(C): 分布式环境下,所有节点保存的数据必须要完全一致,这里的一致包括多个副本之间数据一致,以及类似下单操作,必须确保库存扣减,金额扣减,积分增加在下单操作完成后,保持逻辑一致。
可用性(A): 服务要一直保持可用,对于用户请求要在指定时间内给出响应,返回给用户的响应结果不能是异常信息,要是正常结果。
分区容忍性(P): 分布式系统要能够在出现网络分区故障情况下,依然正常对外提供服务。
数据的一致性又分为强一致性,弱一致性和最终一致性,弱一致性允许存在部分数据不一致。
所有分布式系统都需要具备分区容忍性,因此对于分布式系统而言,我们通常是在A和C之间进行取舍,也就是我们常说的AP和CP之间进行取舍。
Base理论
BASE理论的核心思想即使无法做到强一致性,也可以采用适当的方式达到最终一致性。
BASE理论核心有三点:
基本可用: 在流量高峰期,可以停用非核心功能服务,全力支持核心服务运转
软状态: 在无法确保强一致性的情况下,分布式系统中必然存在中间状态,例如: 多个节点数据同步存在延时
最终一致性: 允许存在短暂的中间状态,,但是最终还是需要确保系统达到一致性状态
BASE理论主要是对AP的补充。
分类
按照CP和AP模型,我们可以将分布式事务分为刚性事务和柔性事务两种类型。
刚性事务强调分布式系统中数据的强一致性,遵循CP原则。
柔性事务强调分布式系统可用性,遵循AP原则和BASE理论。
刚性事务主要的解决方案有:
XA协议--落地实现方案有: 2pc,3pc,JTA,JTS,3PC
柔性事务主要解决方案有:
补偿型: TCC,SAGA
异步通知型: 本地事务消息表,MQ事务消息
XA协议
XA协议规范了分布式事务模型,该模型使用两阶段提供(2pc)来保证分布式事务的完整性。
XA协议规范的分布式事务模型主要包含三个角色:
AP: 应用程序,事务具体包含哪些操作通常定义在每个服务的service方法中,AP执行service方法时,发起事务请求,此时开启了一个分支事务。
TM:事务管理器,接收的AP发起的分支事务请求,并对所有分支事务进行管理。
RM: 资源管理器, XA协议中涉及到的事务提交和回滚能力是由底层的数据库,消息队列或者其他资源管理器提供的。
如果是微服务场景下,通常是一个AP管理着一个RM。
如果是某个服务中涉及多个RM操作,那就是一个AP管理着多个RM了。
2PC
2pc协议主要涉及到两个角色:
协调者(TM)
参与者(RM)
两阶段分为:
prepare: 协调者询问各个参与者是否准备好提供各自的分支事务,参与者收到prepare请求后,执行相关事务操作,并向协调者报告执行的结果
commit/rollback: 协调者收集各个参与者的响应结果,如果其中一个响应NO,或者协调者等待参与者响应超时,此时协调者发送rollback请求,否则发送commit请求。
2pc缺陷:
阻塞问题: 分布式事务执行过程中,所有参与节点都是事务阻塞型的,因为需要等待所有参与者都响应后,才会继续进行下一步操作,因此阻塞时间会由最长的分支事务决定,锁定时间越长,对于数据库相关锁资源,连接资源占用也越长。
单点故障: 协调者是单节点,如果协调者挂了,那么各个参与者将无法释放事务资源,因为协调者端并没有超时等待机制,它只会傻傻的等待协调者的命令。
丢失信息: 协调者发送commit或者rollback请求时,如果存在部分commit请求没有成功发送,那么会存在只有部分参与者执行commit或者rollback成功的情况。
过于保守: 容错机制不完善,当参与者出现故障时,协调者无法及时检测到,只能依赖超时机制来决定是提交还是中断事务。
3pc
3PC针对2PC做出了优化,改善了2pc单点故障问题。
3pc分为了三个阶段:
CanCommit : 协调者询问各个参与者是否可以执行事务提交
PreCommit :本阶段需要根据CanCommit阶段执行结果,决定下一步是执行事务预提交还是中断事务执行。 如果所有参与者在CanCommit阶段均返回ok响应,那么协调者发出PreCommit请求,参与者收到请求后,执行事务操作,并响应执行结果。 如果某个参与者在CanCommit阶段返回no或者迟迟不响应,导致响应超时,协调者会向所有参与者发送Abort请求,中断事务。
doCommit: 如果PreCommit阶段各个参与者均响应yes,那么协调者发出doCommit请求,否则发出rollback请求。
3pc和2pc有一个重要区别就是在参与者端也引入了超时机制,并且把prePare阶段拆分为了,先询问,再锁资源,最后真正提交。
为什么3pc能够解决2pc的单点故障问题呢?
3pc在参与者端引入了超时等待机制,如果在doCommit阶段,参与者无法在有限时间内收到协调者的doCommit或者rollBack请求,会在等待超时后,继续进行事务提交。
因为3pc新增了CanCommit阶段,该阶段应该预留好事务需要的资源,确保PreCommit阶段执行成功,因此在进入了doCommit阶段时,由于网络等原因,虽然参与者没有收到commit或者rollback请求,但是它有理由相信,本次成功提交的几率很大。
3pc缺陷:
数据一致性问题: 如果协调者发出的是rollback请求,但是因为网络原因,部分参与者没有及时收到,而选择在等待超时后,进行事务提交,这样会导致数据不一致问题。
柔性事务
柔性事务解决方案主要分为: 同步补偿型和异步通知型。
同步补偿型解决方案: TCC和SAGA
异步通知型解决方案: MQ事务消息和本地消息表
异步通知型
异步通知型方案解决思路为:
业务执行过程中,会发布相关领域事件,对应的领域事务会被封装为消息发送到消息队列,再由消息队列将消息发送给消息订阅方进行消费。
例如:订单服务接收到订单操作后,将下订单的消息发送到消息队列,同时订单服务将订单信息保存到本地的订单表中;库存服务,积分服务再收到消息队列推送的订单消息后,取出进行处理,进行库存扣减和积分增加操作。
MQ事务消息
上面提到了异步通知型事务常见解决思路,该思路有个问题,在于如何确保本地事务执行和消息投递一致性,也就是要么都成功,要么都失败。支持事务消息的消息队列,如RocketMq和ActiveMq就是解决这个问题的。
Rabbitmq不支持事务消息,并且注意这里事务消息解决的不是消费者成功消费消息和本地事务执行的一致性。
RocketMq采用半消息机制来实现投递消息和参与者本地事务执行的一致性:
事务发起方将消息发送到MQ,此时MQ保存消息,但是扣压不发送
MQ通知发送方消息发送到消息队列保存成功
发送方收到成功消息后,执行本地事务,并将结果响应给消息队列
如果本地事务执行成功,MQ将扣压的消息发送给订阅方
如果本地事务执行失败,MQ丢弃先前保存的消息
如果本地事务执行成功,MQ发送消息给订阅方了,订阅方根据消息执行自己的本地事务
如果订阅方本地事务执行成功,那么响应给MQ ack,告知对应消息已经被成功消费
订阅方消息消费成功机制由MQ保证,但及时如此,也不能保证消息一定被成功消费,因此如果要确保万无一失,还需要在订阅方消费失败时,通过补偿机制,让消息生产方和其他消费消息成功的消费者回滚状态。
如果事务发起方执行本地事务过程中挂掉,或者MQ服务器迟迟等不到响应超时,那么MQ服务器会不断询问事务发起方事务执行状态。
这个不断询问的过程被称为消息反查机制:
本地消息表
如果使用的MQ不支持事务消息,或者我们希望减少代码侵入,可以尝试基于DB的本地消息表方案。
该方案采用数据表持久化存储事务消息,确保在事务消息未被成功投递前,不会从事务消息表中删除。
消息发送方:
准备一个消息表,记录消息相关状态
确保业务数据和消息表在同一个数据库,从而确保它俩在同一个本地事务,利用本地事务将业务数据和事务消息直接写入数据库。
将消息写入消息表后,可以通过专门的投递线程将消息投递到mq,根据投递ack结果去删除事务消息表记录
mq将消息发送给消费者消费
消息消费方:
处理消息队列中的消息,执行自己的本地事务
如果本地事务执行成功,那么ack,告诉mq消息消费成功
如果本地事务执行失败了,那么重试执行
如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚
消息生产方要定时扫描自己的本地消息表,把还没有处理完成的消息或者发送失败的消息继续发送.
消费方同时也需要不断扫描自己的业务补偿消息表。
两种解决方案对比
共性:
都依赖MQ,都是异步的
消息都存在重复投递可能,需要在投递方尽量降低重复投递次数,消费方需要确保消息消费的幂等性。
都需要自己编写业务补偿代码
不同:
MQ事务消息需要对应MQ组件支持半消息机制,半消息机制本身对消息重复投递也有很好的处理,但是该方案对于业务的侵入性比较大。
DB事务消息表更容易造成重复投递,而且消息表本身也会耦合到业务系统中,不方便封装成独立组件。
补偿性型
补偿型分布式事务解决方案的思路为:
首先将分布式事务中各个分支事务变成单独的本地事务执行
由协调服务通知各个微服务执行自己的本地事务,如果各个参与者都回复本地事务执行成功,那么直接本次事务结束
如果存在某个参与者执行本地事务失败,那么发送rollback请求通知所有参与者
参与者接收到rollback请求后,调用业务层面实现的补偿方案,来回滚事务,例如: 执行delete操作,将原先执行成功的insert记录删除
补偿型事务主要解决方案有两种:
TCC
SAGA
TCC(Try-Cofirm-Cancel)
TCC把事务运行过程分成了try,confirm/Cancel两个阶段,每个阶段由程序员编写业务代码控制,避免了长事务,可以获取更高的性能。
TCC事务模型主要包括三部分:
主业务服务: 业务活动的发起方,例如: 订单服务中下订单的操作涉及对库存服务和积分服务的修改,因此首先为订单服务发起本次分布式事务
从业务服务: 业务活动的参与方
业务活动管理器: 维护TCC全局事务状态和每个从业务服务的子事务状态,并在业务提交时调用所有从业务服务的Confirm操作,在业务活动取消时调用所有业务服务的Cancel操作。
TCC分布式事务模型相对于XA而言,主要区别在于它不依赖资源管理器(RM)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
TCC流程:
Try阶段: 由主业务服务发起业务活动,调用参与到本次业务活动服务的try接口,包括自身,完成所有业务检查,预留业务资源。
Confirm操作: 对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用try阶段预留的业务资源。
Cancel操作: 在业务系统执行出错,需要回滚的状态下执行业务取消,释放预留资源。
TCC中会添加事务日志,如果confirm或者cancel出错,则会进行重试,所以这两个阶段需要支持幂等,如果重试失败,如果人工介入进行回复和处理。
在TCC模型中,try接口是主业务服务调用,confirm/cancel接口是事务协调器调用。这就是 TCC 分布式事务模型的二阶段异步化功能,从业务服务的第 一阶段执行成功,主业务服务就可以提交完成,然后再由事务管理器框架异步的执行各从业务服务 的第二阶段。这里牺牲了一定的隔离性和一致性的,但是提高了长事务的可用性。
例子:
某笔订单完成时,同时扣掉用户的现金,但交易未完成,也未被取消时,不能让客户看到钱变少了。
这时我们可以引入TCC,其流程如下:
·订单服务创建订单
·订单服务发送远程调用到现金服务,冻结客户的现金
·提交订单服务数据
·订单服务发送远程调用到现金服务,扣除客户冻结的现金
以上是正常完成的流程,若为异常流程,则需要发送远程调用请求到现金服务,撤销冻结的金额。以上流程比基于补偿实现的事务的流程要复杂,同时开发的工作量也更多:
·订单服务编写创建订单的逻辑
·现金服务编写冻结现金的逻辑
·现金服务编写扣除现金的逻辑
·现金服务编写解冻现金的逻辑
TCC实际上是最为复杂的一种情况,其能处理所有的业务场景,但无论出于性能上的考虑,还是开发复杂度上的考虑,都应该尽量避免该类事务。
TCC事务模型要求:
1.可查询操作:服务操作具有全局唯一的标识,操作唯一的确定的时间。
2.幂等操作:重复调用多次产生的业务结果与调用一次产生的结果相同。一是通过业务操作实现幂等性,二是系统缓存所有请求与处理的结果,最后是检测到重复请求之后,自动返回之前的处理结果。
3.TCC操作:Try阶段,尝试执行业务,完成所有业务的检查,实现一致性;预留必须的业务资源,实现准隔离性。Confirm阶段:真正的去执行业务,不做任何检查,仅适用Try阶段预留的业务资源,Confirm操作还要满足幂等性。Cancel阶段:取消执行业务,释放Try阶段预留的业务资源,Cancel操作要满足幂等性。
TCC与2PC(两阶段提交)协议的区别:TCC位于业务服务层而不是资源层,TCC没有单独准备阶段,Try操作兼备资源操作与准备的能力,TCC中Try操作可以灵活的选择业务资源,锁定粒度。TCC的开发成本比2PC高。实际上TCC也属于两阶段操作,但是TCC不等同于2PC操作。
4.可补偿操作:Do阶段:真正的执行业务处理,业务处理结果外部可见。Compensate阶段:抵消或者部分撤销正向业务操作的业务结果,补偿操作满足幂等性。约束:补偿操作在业务上可行,由于业务执行结果未隔离或者补偿不完整带来的风险与成本可控。实际上,TCC的Confirm和Cancel操作可以看做是补偿操作。
小结:
和XA这种在资源层面实现的分布式事务,在2pc过程中一直持有资源锁,并且需要对应数据源支持XA协议不同,TCC是在业务层面实现的分布式事务,该过程中不会一直持有资源锁,但是每个微服务必须在业务层面实现try,confirm,cancel三个方法,代码侵入性高,并且相关接口还必须实现幂等性。
SAGA
SAGA可以看做一个异步的,利用队列实现的补偿事务。
核心思路: 将分布式事务转换为一个个本地事务,并将这些本地事务组成一个事务链,每个本地事务都有相应的执行模块和补偿模块,当Saga中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。
此部分待完善,后续关于Seata的SAGA模式也会暂时跳过讲解
小结
本文主要介绍了分布式事务概念和相关解决方案,限于篇幅原因,将原定本文讲解的seata落地实现产品,放到下一篇进行介绍,大家敬请期待。
以上是关于分布式事务常见解决方案的主要内容,如果未能解决你的问题,请参考以下文章