1 引言
随着服务向微服务架构、云部署发展,业务中分布式事务场景几乎无可避免。对于分布式事务的原理、正确使用,是一个技术难点,本文就围绕着事务、分布式事务和分布式事务的处理来展开。
2 数据库本地事务
2.1 什么是事务
事务是对一组要么都成功、要么都失败操作的抽象,事务成功执行后事务所变更的数据不会丢失,事务失败后数据要回到事务开始之前的模样。
2.2 事务的最终目的
事务的最终目的是数据的一致性。
2.3 如何保障数据的一致性
事务具备ACID4大特性,C一致性是最终目的,其实现是通过AID。
- 原子性(Atomicity):事务的原子性确保动作要么全部完成,要么完全不起作用。
- 隔离性(Isolation):是说多个事务之间的操作不会相互影响, 它们之间是相互隔离的;
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中。
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败
2.4 AID的实现
A的实现:
MySQL数据库提供了一个用于记录历史数据的日志 undo log。 这个日志在事务执行前,首先对事务相关的原始数据进行记录,当事务需要进行回滚撤销之前已经完成的操作时,通过undo log就可以把数据恢复到事务开始之前。
I的实现:
MySQL中,事务的隔离性是通过数据库锁的机制实现的。
D的实现:
持久性需要解决的问题在于两个方面:
- 事务数据写磁盘是一个异步的过程,事务提交之后,此时事务产生的数据还也许在内存,没有写到磁盘去,此时发生故障会导致数据丢失。
- 调用磁盘写入数据的操作并不具备原子性,所以如果发生故障,那么就有可能存在一部分操作成功,一部分操作失败的情况。
为了解决这个问题,提供了一种数据重做日志(redo log),在事务提交之前,首先会把事务需要变更的数据提前以日志的形式记录到磁盘中去(因为日志是以顺序IO的形式写入的,所以性能非常高),这样在事务提交之后就算发生故障,事务所产生的数据丢失了,那么也可以通过之前记录的redo log 来对数据进行重做。
2.5 本地事务总结
在MySQL中,事务的实现方式是Two Phase Lock(2PL),所有事务必须分两个阶段对数据项加锁和解锁:
1) 在对任何数据进行读、写操作之前,要申请获取对该数据的封锁;
2)每个事务中,所有的封锁请求优先所有的解锁请求。
基于上面两点,事务的处理发展有:
- 串行执行, 一个一个的执行事务排队执行,优点是不需要冲突控制,也不会发生死锁,缺点就是效率低。
- 排它锁,对于不同数据的事务单元,用排它锁并行处理,共享数据的处理,仍串行执行。
- 读写锁,常见的方式有:1)只增加写锁,读不加锁,但可能出现脏读(Read UnCommitted);2)增加读锁和写锁,但读锁可以升级为写锁,可能出现不可重复读(Read Committed);3)增加读锁和写锁,但读写、写写互斥,可能出现幻读。4)MVCC多版本并发控制,写操作是不直接覆盖原本数据的,而是在一个新的版本中写入数据,而读的时候则可以从旧的版本中读取数据。这种方式可以在读读不阻塞的前提下,实现读写/写读不阻塞,尽可能保证所有的读操作并行,而写操作串行。
3. 分布式事务
目前,数据库级别仅支持单库事务,并不支持夸库事务(实质是跨进程)。在微服务架构下,一个业务系统需要由很多个子系统共同完成,而且有些操作需要在一个事务中完成。通俗意义上来讲,分布式事务就是夸数据库的事务支持。分布式事务最大的挑战在于CAP,其来源在于网络分割P的存在,用户不得不在一致性C和可用性A之间权衡。
3.1 XA事务模型
单进程每个点上都能保障自己的ACID特征,XA的核心目标是解决如何协调多个节点之间的操作一致性。
XA定义了两种角色,全局的事务管理器(Transaction Manager) 和局部资源管理器(resource Manager)。全局事务管理器是来协调各个节点统一操作的角色,通常也称为事务协调者。局部资源管理器也就是参与事务执行的进程,通常也称为事务参与者。 事务的执行过程由协调者统一来决策,其它节点只需要按照协调者的指令来完成具体的事务操作即可。而协调者在协商各个事务节点的过程中、什么情况下决定集体提交事务,什么情况下又决定集体回滚事务,这里取决于XA事务模型里使用了哪种协商协议(2PC、3PC)。
3.1.1 基于XA协议的两阶段提交方案
两阶段提交协议的每一次事务提交分为两个阶段:
- 在第一阶段,协调者询问所有的参与者是否可以提交事务(请参与者投票),所有参与者向协调者投票。
- 在第二阶段,协调者根据所有参与者的投票结果做出是否事务可以全局提交,并通知所有的参与者执行该决定。
在一个两阶段提交流程中,参与者不能改变自己的投票结果。两阶段提交协议的可以全局提交的前提是所有的参与者都同意提交事务,只要有一个参与者投票选择放弃(abort)事务,则事务必须被放弃。
两阶段协议的异常处理:
参与者挂掉:
如果在第一阶段,协调者发送Prepare指令给所有的参与者后,参与者挂掉了,此时协调者因为迟迟收不到参与者的消息而导致超时,所以协调者在超时之后会统一发送abort指令进行事务回滚。
如果在第二阶段,协调者发送commit或者abort指令给所有参与者后,参与者挂掉了,那么协调者会在超时之后进行消息重发,直到参与者恢复后收到到commit或者abort ,向协调者返回成功。
协调者挂掉:
协调者在第一阶段发送Prepare指令后挂掉,此时参与者一直得不到协调者下一步的指令,所以参与者会一直陷入阻塞状态,资源也会一直被锁住,直到协调者恢复之后向参与者发出下一步的指令。
协调者在第二阶段挂掉,此时协调者已向所有者发出最后阶段的指令了,所以收到指令的参与者会完成最后的commit或rollback操作,对于参与者来说事务已经结束,所以不存在阻塞和锁的问题, 当协调者恢复后,会把事务日志状态标记为结束。
极端情况下的数据不一致:
协调者在第二阶段向部分参与者发送了commit指令后挂了或者一部分参与者因为网络问题没收到指令,那么此时收到了commit指令的参与者会进行事务提交,然后未收到消息的参与者还是等着协调者的指令,所以这个时候会产生数据的不一致,此时必须要等协调者恢复之后重新发送指令,参与者才能达到最终的一致状态。
2PC的遗留问题:
- 同步阻塞:在二阶段提交的过程中,所有的节点都在等待其他节点的响应,这种同步阻塞极大的限制了分布式系统的性能。
- 单点问题:协调者在整个二阶段提交过程中很重要,如果协调者在提交阶段出现问题,那么整个流程将无法运转。更重要的是,其他参与者将会处于一直锁定事务资源的状态中,而无法继续完成事务操作。
- 数据不一致:若当协调者发送commit请求之后,发生了局部网络异常,或者是协调者在尚未发送完所有 commit请求之前自身崩溃,导致最终只有部分参与者收到了commit请求,这将导致严重的数据不一致问题。
- 容错性不好:在二阶段提交的提交询问阶段中,参与者出现故障,导致协调者始终无法获取到所有参与者的确认信息,这时协调者只能依靠其自身的超时机制,判断是否需要中断事务。
3.1.2 3PC事务提交
相对于2PC来说,3PC增加了一个询问阶段,然后准备阶段和2PC的准备阶段大致类似。
询问阶段:
协调者开启事务事务,向所有参与者发送CanCommit 指令询问参与者是否具备事务执行条件,参数者收到指令后,判断是否具备事务执行条件,然后向协调者响应成功或失败。协调者收到任何一个参与者失败响应,此时会协调者会记录全局事务信息(事务信息、状态为abort),然后向所有参与者发送abort指令。只有当所有参与者都响应成功之后,此时进入第二阶段,协调者首先会记录全局事务信息(事务信息、状态为Precommit),然后向所有参与者发送precommit指令。
准备阶段:
协调者向所有参与者发送precommit指令。参与者收到指令后,会开启本地事务,锁定事务对应的资源,然后记录事务日志(Redo log Undo log),最后参与者根据本地事务的结果向协调者响应成功或失败。当协调者收到任何一个参与者响应失败,此时协调者会记录全局事务信息(事务信息、状态为abort),然后向所有参与者发送abort指令。只有当所有参与者都响应成功之后,此时进入第三阶段,协调者首先会记录全局事务信息(事务信息、状态为commit),然后向所有参与者发送docommit指令。
这里与2PC不同的是,进入准备阶段后,如果参与者迟迟没有收到协调者的消息(网络分区或协调者故障),此时参与者会有超时机制,当参与者超时之后会执行统一默认策略进行事务commit。
提交阶段:
如果所有参与者都响应成功,协调者会决定提交事务,首先会记录全局事务信息状态为commit,然后向所有参与者发送commit指令,参与者收到commit指令后会对上一阶段生成的事务信息进行最后的commit。最后协调者收到所有参与者的成功响应后,将全局事务信息记录为事务完成,否则任意一个协调者返回失败或者超时,协调者都会一直进行重试,直到成功。
3.1.3 3PC与2PC对比
- 性能问题(相比2PC性能更差)。2PC锁定资源,时间取决于最慢的一个参与者,在3PC里这样的情况并未发生任何变化,而3PC还增加了一个阶段的协商通讯,这就使得3PC通信成本更高,性能反而会更差。
- 能解决协调者故障导致的事务阻塞问题。因为3PC增加了询问阶段,然后在准备阶段增加了参与者超时机制,所以协调者故障并不会一直阻塞着事务进行,参与者超时之后会进行事务commit。
- 还是存在数据不一致的风险。如果协调者第二阶段的决策是abort,此时协调者把abort指令发送给了部分参与者之后挂掉了,那么收到了abort指令的参与者进行了数据回滚,但是没有收到abort指令的参与者会根据超时机制进行事务commit,最终就会有部分参与者rollback了,部分参与者进行了commit,最后数据不一致。
3.2 不可能完美的CAP理论
在一个分布式系统中,它的一致性、可用性、分区容错性同时只能满足两个,必然会牺牲一个。在分布式系统中因为网络分区的必然性,所以只能在A和C中选择一个。
3.3 CAP不完美的接受BASE理论
Base理论是基于最终一致性模型,提出的一套实践理论,Base理论从基本可用、软状态、最终一致性 三个层面指导我们进行分布式系统设计。
-
基本可用(Basically Available):系统发生故障时,允许牺牲一部分功能的可用性。
-
软状态(Soft State):允许系统存在一些中间的状态,因为网络有延时,而这些延时可能会导致部分数据存在一定时差的不一致;
-
最终一致性(Eventually Consistent):系统可以允许一段时间的不一致,但是经过一段时间后,最终数据要恢复一致。
3.4 给予BASE理论的分布式事务解决方案
3.4.1 TCC协议实现分布式事务
TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖资源管理器(RM)对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
第一阶段:CanCommit
- 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
- 响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
第二阶段:PreCommit,协调者获取第一阶段的询问结果后,会执行两种操作:执行事务预提交,或者中断事务
- 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
- 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
- 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者返回No,或者等待超时后,协调者没有收到所有参与者的响应,执行事务中断。
- 发送中断请求:协调者向所有参与者发送abort请求。
- 中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
第三阶段:Do Commit,真正执行事务提交,也会分为两种情况
提交事务:
- 发送提交请求:协调接收到参与者发送的ACK响应,从预提交状态进入到提交状态,并向所有参与者发送doCommit请求
- 事务提交:参与者接收到doCommit请求之后,执行正式的事务提交,在完成事务提交之后释放所有事务资源。
- 响应反馈: 事务提交完之后,向协调者发送Ack响应。
- 完成事务:协调者接收到所有参与者的ack响应之后,完成事务。
中断事务:协调者没有接收到参与者发送的ACK响应或响应超时,执行中断事务。
- 发送中断请求:协调者向所有参与者发送abort请求
- 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
- 反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息
- 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
相对于二阶段的XA协议,TCC主要在单点故障和同步阻塞上做了改进,并且在无法收到来自协调者的信息时,会默认执行commit,而不会一直持有事务资源。但其缺点有:
- 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。
- 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。
3.4.2 SAGA协议实现分布式事务
Saga的组成:1)每个Saga由一系列sub-transaction Ti 组成;2)每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果。
Saga的执行顺序有:
- T1,T2,...,Tn;所有的子事务都正常执行
- T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n。有部分子事务通过补偿事务被执行
Saga中要求Ti和Ci是幂等的,并且Ci是能够成功的,如果不成功则需要人为介入。此外,Ti - Ci和Ci - Ti的执行结果必须是一样的,也就是说子事务和子补偿的执行结果要一致。SAGA的ACID特性
- 原子性:通过SAGA协调器实现
- 一致性:本地事务+SAGA Log
- 持久性:SAGA Log
- 隔离性:不保证(同TCC)
3.4.3 给予消息的最终一致性事务
事务发起方在完成本地事务后,会向调用方发送一个消息,收到消息的事务参与者会执行本地事务,多个事务合作的逻辑依次类推。 各个事务之间通过消息通知达实现分布式事务的操作,达到数据最终一致性。
消息事务模式需要注意:
- 如何保证本地事务完成后,消息一定成功发送到下游服务。
- 接收消息的服务要具备业务操作的幂等性。