【事务】:是由一系列数据库操作构成的逻辑单元,事务中的操作要么全部成功,要么全部失败,事务就是为了保证数据的最终一致性。
【本地事务】:是基于关系型数据库的事务处理机制,指在一个单一的服务或应用中,对单一数据库资源进行的访问和修改操作所组成的事务。本地事务严格遵循 ACID
特性,即原子性、一致性、隔离性和持久性。
【分布式事务】:是指事务的发起者、资源及资源管理器和事务协调者分别位于分布式系统的不同节点之上。在分布式系统中,一次大的操作往往由多个小操作组成,这些小操作分布在不同的服务器上,分布式事务需要确保这些操作要么全部成功,要么全部失败,以保证数据的一致性和完整性。
本质上来说,分布式事务就是为了保证在分布式场景下,数据操作的正确执行。
CAP
理论可以表述为,一个分布式系统最多只能同时满足一致性(Consistency
)、可用性(Availability
)和分区容忍性(Partition Tolerance
)这三项中的两项。
【一致性】:指所有节点同时看到相同的数据,即更新操作成功后所有节点拥有数据的最新版本。
【可用性】:指任何时候读写都是成功的,即服务一直可用,而且是正常响应时间。
【分区容忍性】:指当部分节点出现消息丢失或者分区故障的时候,分布式系统仍然能够继续运行。
在分布式系统中,由于系统的各层拆分,P
是确定的,CAP
的应用模型就是CP
架构和AP
架构。分布式系统所关注的,就是在P
的前提下,如何实现更好的A
和更稳定的C
。
Base
是三个短语的简写,即基本可用(Basically Available
)、软状态(Soft State
)和最终一致性(Eventually Consistent
)。
Base
理论的核心思想是最终一致性,即使无法做到强一致性(Strong Consistency
),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual Consistency
)。
【基本可用】:系统能够基本运行,一直提供服务,不追求CAP
中的任何时候读写都是成功的。基本可用强调了分布式系统在出现不可预知的故障时,允许损失部分可用性。
【软状态】:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
【最终一致性】:数据不可能一直是软状态,必须在一个时间期限之后达到各个节点的一致性,在期限过后,应当保证所有副本保持数据一致性,也就是达到数据的最终一致性。
在系统设计中,最终一致性实现的时间取决于网络延时、系统负载、不同的存储选型、不同数据复制方案设计等因素。
【强一致性】:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值。
【弱一致性】:系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
【最终一致性】:最终一致性是弱一致性的特例,强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。
系统设计时,选择实现强一致性还是弱一致性要依赖于业务场景。
为了保证该事务可以满足ACID
,就要引入一个协调者(Cooradinator
),其他的节点被称为参与者(Participant
)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
第一阶段:准备阶段(prepare phase)
在准备阶段,协调者向所有参与者询问是否准备好提交事务,如果所有参与者都准备好了,协调者发出提交请求,否则协调者发出中止请求。
第二阶段:提交阶段(commit phase)
在提交阶段,所有参与者根据协调者的请求执行提交或中止操作。
优点:保证了事务的原子性和一致性,适用于多个节点的数据更新操作。
缺点:存在阻塞问题,如果协调者故障,可能导致参与者一直处于阻塞状态。同时由于网络问题或参与者故障,可能导致无法提交或回滚事务,并且其效率较低。
三阶段提交是在两阶段提交上的改进版本,三阶段提交最关键要解决的就是协调者和参与者同时挂掉的问题,所以三阶段提交在两阶段提交的基础上增加了canCommit
阶段。
第一阶段:canCommit阶段(canCommit phase)
在canCommit
阶段,协调者询问所有参与者是否可以顺利执行事务,如果所有参与者都可以执行,协调者发出执行事务请求,否则发出中止请求。
第二阶段:preCommit阶段(preCommit phase)
在preCommit
阶段,如果所有参与者都执行完事务并准备提交事务,协调者发出提交请求,否则协调者发出中止请求。
第三阶段:doCommit阶段(doCommit phase)
在doCommit
阶段,所有参与者根据协调者的请求执行提交或中止操作。
优点:相比两阶段提交,在等待超时后协调者或参与者会中断事务,降低了阻塞问题的发生概率。
缺点:虽然降低了阻塞问题,但仍然存在可能的阻塞和超时问题。相比两阶段提交,三阶段提交通过canCommit
阶段减少了阻塞的可能性,但同样存在性能开销和单点故障问题。
两阶段提交和三阶段提交的区别:
1)、引入超时机制,同时在协调者和参与者中都引入超时机制。两阶段提交只有协调者有超时机制,超时后发送回滚指令。三阶段提交协调者和参与者都有超时机制。
两阶段提交:协调者超时发送回滚指令。
三阶段提交:
协调者超时:canCommit
和preCommit
中,如果收不到参与者的反馈,则协调者向参与者发送中断指令。
参与者超时:preCommit
阶段,参与者进行中断。doCommit
阶段,参与者进行提交。
2)、三阶段提交比两阶段提交多了询问阶段,可以尽早地发现问题,从而避免了后续的阻塞和无效操作,发生阻塞的几率变小了。
TCC
事务补偿是基于两阶段提交实现的业务层事务控制方案,它是Try
、Confirm
和Cancel
三个单词的首字母,含义如下:
1)、尝试阶段Try
:执行所有业务检查,尝试预留必须的资源。
2)、确认阶段Confirm
:执行真正的提交操作,释放资源。默认该阶段是不会出错,只要Try
成功,Confirm
一定成功。
3)、取消阶段Cancel
:执行回滚操作,释放预留的资源。
下一个订单减一个库存案例:
1)、Try
阶段:订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量- 1。
2)、如果Try
阶段执行成功,执行Confirm
阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。
3)、如果Try
阶段执行失败,执行Cancel
阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
优点:提供了更细粒度的事务控制,可以灵活处理分布式事务。
缺点:实现较为复杂,需要开发者自行实现事务的尝试、确认和取消操作。
应用:TCC
通过业务逻辑上的补偿机制来保证分布式事务的一致性,适用于业务逻辑允许回滚的场景。
本地消息表是最简单实用的分布式事务方案,基本上所知的分布式事务场景,都可以拆成本地消息表的方式执行,最后达到最终一致性。
本地消息表将分布式事务拆分成一个个依赖于本地的数据库事务,然后以异步的方式达到最终一致性,消费方
需要注意幂等
性问题。
服务A在执行本地事务时,将消息写入本地消息表,并通过异步方式通知服务B处理消息,服务B处理完成后,通过回调等方式通知服务A。
以传统的订单下发、扣除库存为例:
1)、插入订单记录时,保存一条消息到本地消息表中,状态字段变为未发送,保证原子性,然后将消发送到MQ
,提供方要确保插入订单记录和插入订单本地消息表要在同一事务下面。
2)、消息发送成功,执行Confirm
的回调,修改消息表中的状态为已发送。如果消息发送失败,需要进行重试。
3)、消费方采用手动ACK机制+重试机制
,接收到消息后先进行幂等性判断,然后再处理自己的逻辑。消费方执行成功,本地消息表状态改为成功。消费方异常,本地事务回滚,触发重试,重试失败,ACK无法签收(一种是设置过期,最后进行入死信队列,另一种是回复nack重新发送再次处理),本地消息表状态改为失败。如果执行失败需要想办法通知订单系统也回滚(看业务是否需要回滚),或者是发送报警由人工来手工回滚和补偿。
4)、为了避免订单系统消息发送不成功的情况,可以提供一个补偿的定时任务,轮询扫描订单的本地消息表,找出所有未发送的记录,进行消息发送。这种方案还是非常实用的。
优点:实现逻辑简单,开发成本比较低。
缺点:消息数据和业务数据耦合,消息表需要根据具体的业务场景制定,不能公用。就算可以公用消息表,对于分库的业务来说每个库都是需要消息表的。
场景:适用于异步执行的业务,且后续操作无须回滚的业务。适用于对实时性要求不高的场景,通过异步方式确保最终一致性。例如在A -> B场景下,在不考虑网络异常、宕机等非业务异常的情况下,A成功的话,B肯定也会成功的。
事务消息是通过消息中间件来解耦本地消息表和业务数据表,适用于所有对数据最终一致性需求的场景。现在支持事务消息的消息中间件只有RocketMQ
,这个概念最早也是RocketMQ
提出的。
通过事务消息实现分布式事务的流程如下:
1)、发起方发送半事务消息会给RocketMQ
,此时消息的状态prepare
,接受方还不能拉取到此消息。
2)、发起方进行本地事务操作。
3)、事务执行成功,commit
,发起方给RocketMQ
确认提交消息,此时接受方可以消费到此消息了。事务执行失败,rollback
,从RocketMQ
中删除这条消息。
4)、消费端接收到消息进行消费,如果消费失败,需要进行重试。如果无法通过重试成功,那么还需要更多的机制,来回滚操作或者是进行补偿。
5)、RocketMQ
会定期扫描还没确认的消息,回调给发送方,询问此次事务的状态,根据发送方的返回结果把这条消息进行取消还是提交确认。
优点:长事务仅需要分拆成多个任务,并提供一个反查接口,使用简单。
缺点:消费者的逻辑如果无法通过重试成功,那么还需要更多的机制,来回滚操作。
场景:适用于可异步执行的业务,且后续操作无需回滚的业务。
目前国内互联网公司都是这么使用的,要不你就用RocketMQ
支持的,要不你就自己基于类似ActiveMQ
或者是RabbitMQ
自己封装一套类似的逻辑出来,总之思路就是这样子的。
服务A通过一定策略(如重试、定时任务等)尽最大努力将消息通知给服务B,但不保证服务B一定能够收到和处理消息,服务B需要主动向服务A查询消息来满足需求。
1、系统A本地事务执行完之后,发送消息到MQ
。
2、这里会有个专门消费MQ
的最大努力通知服务,这个服务会消费MQ
然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口。
3、要是系统B执行成功就OK了,要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃。
以充值为例子:
1、账户系统调用充值系统接口
2、充值系统完成支付处理向账户系统发起充值结果通知,若通知失败,则充值系统按策略进行重复通知。
3、账户系统接收到充值结果通知修改充值状态。
4、账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
在最大努力通知中,当事务需要提交时,协调者会向所有参与者发送通知请求,然后立即返回。参与者收到通知后,执行提交操作。而在事务需要回滚时,协调者同样向所有参与者发送通知请求,然后立即返回。参与者收到通知后,执行回滚操作。由于通知是异步的,参与者可能由于各种原因导致提交或回滚失败,但通过重试和幂等性保证最终一致性。
解决方案上,发起通知方通过一定的机制最大努力将业务处理结果通知到接收方,最大努力通知需要:
1)、有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。消息队列ACK
机制,消息队列按照间隔1min、5min、10min、30min、1h、2h、5h、10h
的方式,逐步拉大通知间隔,直到达到通知要求的时间窗口上限,之后不再通知。
2)、消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。提供接口,让接受通知方能够通过接口查询业务处理结果。
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
最大努力通知是通过异步通知的方式来实现事务的commit
和rollback
,接收者可能由于各种原因导致提交或回滚失败,但通过重试和幂等性保持一致。
优点:最大努力通知实现简单,对参与者的性能影响较小。
缺点:可能存在数据不一致问题,需要通过重试和幂等性来解决。
场景:适用于对一致性要求不是特别严格的场景,如通知类消息。
Saga
事务模型又叫做长时间运行的事务(Long-running-transaction
),它描述的是另外一种在没有两阶段提交的情况下解决分布式系统中复杂的业务事务问题。该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由Sagas工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas
工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
Saga
也是一种补偿协议,在Saga
模式下,分布式事务内有多个参与者,每一个参与者都是一个补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga
也分为两个阶段:一阶段直接提交本地事务,二阶段成功则什么都不做,失败则通过编写补偿业务来回滚。
优点:
1)、事务参与者可以基于事件驱动实现异步调用,吞吐高。
2)、一阶段直接提交事务,无锁,性能好。
3)、不用编写TCC
中的三个阶段,实现简单,补偿服务易于实现。
缺点:
1)、软状态持续时间不确定,时效性差。
2)、不保证隔离性。没有锁,没有事务隔离,会有脏写。
场景:长事务适用,对中间结果不敏感的业务场景使用。
Seata
是一个开源的分布式事务解决方案,提供了AT
、TCC
、SAGA
等多种事务模式。其中AT
模式基于本地事务和全局锁的机制,确保分布式事务的一致性。
AT
模式是一种无侵入的分布式事务解决方案。在AT
模式下,用户只需关注自己的业务SQL
,用户的业务SQL
作为一阶段,Seata
框架会自动生成事务的二阶段提交和回滚操作。
在一阶段,Seata
会拦截业务SQL
,解析SQL
语义后找到业务SQL
要更新的数据,在数据被更新前,将其保存成before image
,然后执行业务SQL
更新数据,在数据更新之后,再将其保存成after image
,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段如果是提交的话,因为业务SQL
在一阶段已经提交至数据库,所以Seata
框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata
就需要回滚一阶段已经执行的业务SQL
,还原业务数据。回滚方式便是用before image
还原业务数据。但在还原前要首先要校验脏写,对比数据库当前业务数据(看是否有别人又修改了数据)和after image
,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
场景:通过集成Seata
,开发者可以方便地实现分布式事务。Seata
提供了简单易用的API
和丰富的功能,降低了分布式事务实现的复杂度。
在项目中,处理分布式事务是一个关键且复杂的任务。
需要根据项目的具体需求和所使用的技术栈,选择适合的分布式事务解决方案。
首先需要明确分布式事务涉及的各个服务和数据库,并且需要识别出哪些操作需要跨多个服务或数据库进行协调。
在此基础上,采用基于消息队列的事务消息
最终一致性方案来解决分布式事务问题。具体来说使用RocketMQ
作为消息队列中间件。当需要执行分布式事务时,发起方服务会首先在本地执行其业务逻辑,并将需要跨服务协调的消息发送到RocketMQ
的消息队列中。消息队列保证了消息的可靠传输和持久化,即使发送方服务出现故障,消息也不会丢失。接下来,消费方服务会异步地从RocketMQ
中拉取消息,并执行相应的业务逻辑。一旦消费方服务成功处理了消息并完成了其本地事务,它会向消息队列发送一个确认消息,表明该消息已被成功处理。如果消费方服务在处理消息时出现故障,RocketMQ
会根据配置进行重试,确保消息能够被正确处理。
通过这种方式,实现了分布式事务的最终一致性。虽然可能存在短暂的数据不一致状态,但在系统正常运行的情况下,这种不一致状态会很快被修复,不会对业务造成重大影响。
除了基于消息队列的解决方案外,还考虑其他分布式事务处理方案,如两阶段提交(2PC)和三阶段提交(3PC)。然而,由于这些方案存在性能开销较大和单点故障等问题,在项目中并不适用。因此,选择了基于事务消息最终一致性的方案,它更符合项目的需求和特点。
最后,为了确保分布式事务的可靠性,还需要采取了一些额外的措施。对关键操作进行了幂等性
设计,以确保即使消息被重复处理,也不会对业务造成重复影响。此外还建立完善的监控和告警
机制,以便及时发现和处理分布式事务中可能出现的问题。通过以上的分布式事务处理方案和实践,成功地在项目中实现了跨多个服务和数据库的事务一致性,确保了业务的正确性和可靠性。
在分布式事务的各个环节都有可能出现网络以及业务故障等问题,这些问题需要分布式事务的业务方做到防空回滚,幂等,防悬挂三个特性,下面以TCC
事务说明这些异常情况:
【空回滚】:在没有调用TCC
资源Try
方法的情况下,调用了二阶段的Cancel
方法,Cancel
方法需要识别出这是一个空回滚,然后直接返回成功。出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try
阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel
方法,从而形成空回滚。
【幂等】:由于任何一个请求都可能出现网络异常,出现重复请求,所以所有的分布式事务分支,都需要保证幂等性。
【悬挂】:悬挂就是对于一个分布式事务,其二阶段Cancel
接口比Try
接口先执行。出现原因是在RPC
调用分支事务try
时,先注册分支事务,再执行RPC
调用,如果此时RPC
调用的网络发生拥堵,RPC
超时以后,TM
就会通知RM
回滚该分布式事务,可能回滚完成后,RPC
请求才到达参与者真正执行。
【解决】:面对网络异常情况,建议的方案都是业务通过唯一键去查询相关的操作是否已经完成,如果已经完成则直接返回成功。相关的逻辑比较复杂,易出错,业务负担重。