【分布式】分布式事务方案:两阶段、TCC、SEATA
【分布式】分布式事务方案:两阶段、TCC、SEATA
- 1、什么是分布式系统的一致性?
- 2、分布式事务
- 2.1 常见的分布式事务有哪些?
- 2.1.1 如何选择
- 2.2 什么是分布式事务中的两阶段提交(2PC)
- 2.2.1 2PC的优缺点
- 2.2.2 为什么还需要三阶段提交(3PC)?
- 2.3 什么是TCC
- 2.3.1 TCC的空回滚和悬挂是什么?如何解决?
- 2.3.2 TCC中,Confirm或者Cancel失败了怎么办?
- 2.3.3 TCC和2PC有什么区别?
- 2.4 如何基于MQ实现分布式事务?
- 2.4.1 如何基于本地消息表实现分布式事务?
- 2.4.2 定时任务扫表的缺点有什么?
- 2.4.3 为了避免丢消息问题需要落表,如何设计这张消息表?
- 2.4.4 什么是最大努力通知?
- 2.4.5 最大努力通知&事务消息&本地消息表三者区别是什么?
- 2.4.6 什么是事务消息,为什么需要事务消息?
- 2.5 Seata的实现原理是什么
- 2.5.1 Seata的4种事务模式,各自适合的场景是什么?
- 2.5.2 Seata的AT模式和XA有什么区别?
- 2.5.3 Seata的AT模式会不会出现脏读?为什么?
1、什么是分布式系统的一致性?
所谓一致性,是指数据在多个副本之间是否能够保持一致的特性。在聊一致性的时候,其实要搞清楚一致性模型。(概念挺多,但是没办法,这玩意它本身就是理论。想结合代码、示例都做不到,甚至想着画个图都不知道该如何下手)
分布式系统中的一致性模型是一组管理分布式系统行为的规则。它决定了在分布式系统中如何访问和更新数据,以及如何将这些更新提供给客户端。面对网络延迟和局部故障等分布式计算难题,分布式系统的一致性模型对保证系统的一致性和可靠性起着关键作用。在分布式系统中有多种一致性模型可用,每个模型都有其优点和缺点,选择模型取决于系统的具体要求。
大的分类上面,主要有三种,分别是强一致性、弱一致性和最终一致性:
● 强一致性模型(Strong Consistency): 在强一致性模型下,系统保证每个读操作都将返回最近的写操作的结果,即任何时间点,客户端都将看到相同的数据视图。这包括线性一致性(Linearizability)、顺序一致性(Sequential Consistency)和严格可串行性(Strict Serializability)等子模型。强一致性模型通常牺牲了可用性来实现数据一致性。
● 弱一致性模型(Weak Consistency): 弱一致性模型放宽了一致性保证,它允许在不同节点之间的数据访问之间存在一定程度的不一致性,以换取更高的性能和可用性。这包括因果一致性(Causal Consistency)、会话一致性(Session Consistency)和单调一致性(Monotonic Consistency)等子模型。弱一致性模型通常更注重可用性,允许一定程度的数据不一致性。
● 最终一致性模型(Eventual Consistency): 最终一致性模型是一种最大程度放宽了一致性要求的模型。它允许在系统发生分区或网络故障后,经过一段时间,系统将最终达到一致状态。这个模型在某些情况下提供了很高的可用性,但在一段时间内可能会出现数据不一致的情况。
2、分布式事务
2.1 常见的分布式事务有哪些?
分布式事务的目的是保证分布式系统中的多个参与方的数据能够保证一致性。即所有参与者,在一次写操作过程中要么都成功,要么都失败。
至于这个一致性到底是怎样的一致性,是强一致性、还是最终一致性,不同的分布式事务方案其实达到的效果并不相同。
如果想要实现强一致性,那么就一定要引入一个协调者,通过协调者来协调所有参与者来进行提交或者回滚。所以,这类方案包含基于XA规范的二阶段及三阶段提交、以及支持2阶段提交。
如果想要实现最终一致性,那么方案上就比较简单,常见的基于可靠消息的最终一致性(本地消息表、事务消息)、最大努力通知等。TCC也是最终一致性的一种实现方案。只不过他比基于消息的最终一致性要稍微更加靠近强一致性一些,或者说他的不一致性的时长会更短。
还有一些分布式事务的组件,如Seata,他其实是一个开源的分布式事务解决方案,旨在为微服务架构提供高效且透明的事务管理。在讨论 Seata 的一致性特性时,需要明确其支持的不同事务模式,因为每种模式对一致性的保证不同。
● AT 模式:尽管设计为提供强一致性,但在分布式系统中完全实现强一致性是具有挑战性的,尤其是在面对网络分区或节点故障时。因此,尽管AT模式致力于达到强一致性,它在某些故障场景中可能只能保证最终一致性。
● TCC 和 Saga 模式:这两种模式都设计为支持最终一致性。它们允许更大的灵活性和可伸缩性,因为它们通过明确的补偿机制来处理事务的不同阶段,从而逐步达到全局的一致性。
2.1.1 如何选择
在选择一个分布式事务方案的时候,需要考虑很多因素,结合自己的业务来做考量选择。一般来说可以有以下几种选择方式:
1、实现成本:根据项目开发和维护的难度、成本等方面来选择合适的分布式事务方案。这几种方案中,TCC和2PC的实现成本最高,业务侵入性也比较大。
另外,事务消息、本地消息表和最大努力通知都依赖消息中间件,所以如果已有业务已经接入了消息中间件的话,那么使用成本还算可控,否则就需要考虑消息中间件部署、维护和接入成本。而且同样是消息中间件,也不是所有的都支持事务消息,这个也是需要考量的一个重要因素。
2、一致性要求:在一致性方面,2PC、3PC、XA 等等都属于是可以保证强一致性的,而其他的几种方案是最终一致性的方案。
根据业务情况,比如下单环节中,库存扣减和订单创建可以用强一致性来保证。而订单创建和积分扣减就可以用最终一致性即可。而对于一些非核心链路的操作,如核对等,可以用最大努力通知即可。
3、可用性要求:根据CAP理论,可用性和一致性是没办法同时保证的,所以对于需要保证高可用的业务,建议使用最大努力通知等最终一致性方案;对于可用性要求不高,但是需要保证高一致性的业务,可使用2PC等方案。
4、数据规模:对于利用消息中间件的这种方案,其实不是特别适合业务量特别大的场景,有可能出现消息堆积导致一致性保障不及时。对于数据量大的场景,可以考虑Seata方案。
2.2 什么是分布式事务中的两阶段提交(2PC)
在分布式系统中,一个事务可能会跨多个数据库或服务节点。我们需要保证事务的 ACID 特性,即使分布在多个节点上,也要么所有节点都成功提交,要么全部回滚,不能出现部分成功、部分失败的情况。
2PC(Two-Phase Commit,两阶段提交协议) 就是一个经典的分布式事务一致性协议,通过协调者(Coordinator) 和 参与者(Participant) 的配合来实现。参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是终止操作。
协调者(Coordinator):
- 通常就是发起事务的那个应用程序或一个专门的事务管理器。
- 负责驱动整个协议流程,询问所有参与者,并根据反馈做出最终决定。
参与者(Participants):
- 分布式事务中涉及的各个独立的资源管理器。
- 例如:不同的数据库、不同的微服务。
- 它们各自管理自己的本地事务,并执行协调者的指令。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
第一阶段:准备阶段(Prepare Phase)
● 协调者 向所有参与者发送 Prepare 请求,询问能否提交事务。
● 参与者:
- 执行事务,但不提交(只是写入日志或加锁,进入“预提交”状态)。
- 如果成功,返回 “Yes”;如果失败或遇到冲突,返回 “No”。此时事务还没有真正提交,所有参与者都处于 等待协调者指令的状态。
第二阶段:提交阶段(Commit Phase)
● 如果 所有参与者都返回 Yes:
- 协调者向所有参与者发送 Commit 请求。
- 参与者正式提交事务,并释放锁资源。
● 如果 任意一个参与者返回 No(或超时未响应):
- 协调者向所有参与者发送 Rollback 请求。
- 参与者回滚事务,释放锁资源。
2.2.1 2PC的优缺点
2PC的优点比较明显,就是他的概念和流程都比较简单,并且是可以保证强一致性的。
但是缺点也比较明显,最重要的问题是可能会带来数据不一致的问题,除此之外,还存在同步阻塞以及单点故障的问题。
首先看为什么会发生同步阻塞和单点故障的问题:
1、同步阻塞问题。在参与者回复 Yes 后到收到协调者最终指令之前,其资源一直处于锁定状态。此时其他事务无法访问这些资源。如果协调者一直不发送指令,参与者会一直阻塞,严重影响系统性能和可用性。
2、单点故障问题。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
作为一个分布式的一致性协议,我们主要关注他可能带来的一致性问题的。2PC在执行过程中可能发生协调者或者参与者突然宕机的情况,在不同时期宕机可能有不同的现象。
情况一:协调者挂了,参与者没挂
这种情况其实比较好解决,只要找一个协调者的替代者。当他成为新的协调者的时候,询问所有参与者的最后那条事务的执行情况,他就可以知道是应该做什么样的操作了。所以,这种情况不会导致数据不一致。
但是前面说了,这种情况虽然不会有不一致的问题,但是还是会带来阻塞的问题。
情况二:参与者挂了,协调者没挂
这种情况其实也比较好解决。如果参与者挂了。那么之后的事情有两种情况:
● 第一个是挂了就挂了,没有再恢复。那就挂了呗,反正不会导致数据一致性问题。
● 第二个是挂了之后又恢复了,这时如果他有未执行完的事务操作,直接取消掉,然后询问协调者目前我应该怎么做,协调者就会比对自己的事务执行记录和该参与者的事务执行记录,告诉他应该怎么做来保持数据的一致性。
情况三:参与者挂了,协调者也挂了
这种情况比较复杂,我们分情况讨论。
● 协调者和参与者在第一阶段(Prepare之后)挂了。
○ 由于这时还没有执行commit操作,新选出来的协调者可以询问各个参与者的情况,再决定是进行commit还是rollback。因为还没有commit,所以不会导致数据一致性问题。
● 第二阶段协调者和参与者挂了,挂了的这个参与者在挂之前并没有接收到协调者的指令,或者接收到指令之后还没来的及做commit或者rollback操作。
○ 这种情况下,当新的协调者被选出来之后,他同样是询问所有的参与者的情况。只要有机器执行了abort(rollback)操作或者第一阶段返回的信息是No的话,那就直接执行rollback操作。如果没有人执行abort操作,但是有机器执行了commit操作,那么就直接执行commit操作。这样,当挂掉的参与者恢复之后,只要按照协调者的指示进行事务的commit还是rollback操作就可以了。因为挂掉的机器并没有做commit或者rollback操作,而没有挂掉的机器们和新的协调者又执行了同样的操作,那么这种情况不会导致数据不一致现象。
● 第二阶段协调者和参与者挂了,挂了的这个参与者在挂之前已经执行了操作。但是由于他挂了,没有人知道他执行了什么操作
。
○ 这种情况下,新的协调者被选出来之后,如果他想负起协调者的责任的话他就只能按照上面介绍的情况来执行commit或者rollback操作(即询问所有参与者的情况)。这样新的协调者和所有没挂掉的参与者就保持了数据的一致性,我们假定他们执行了commit。但是,这个时候,那个挂掉的参与者恢复了怎么办,因为他之前已经执行完了之前的事务,如果他执行的是commit那还好,和其他的机器保持一致了,万一他执行的是rollback操作那?这不就导致数据的不一致性了么?虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了!
2.2.2 为什么还需要三阶段提交(3PC)?
3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
CanCommit 阶段(询问阶段)
● 协调者向所有参与者发送 CanCommit 请求,询问是否可以执行事务。
● 参与者:
○ 如果可以执行(比如本地检查通过),返回 Yes;
○ 如果不可以执行,返回 No。
这一阶段相当于 2PC 的第一阶段,但只是“问一下”。
PreCommit 阶段(预提交阶段)
● 如果所有参与者都返回 Yes:
○ 协调者向所有参与者发送 PreCommit 请求,进入预提交阶段。
○ 参与者执行事务操作(写日志、加锁),并进入 等待提交状态。
○ 此时参与者可以安全地提交事务,但还没有最终提交。
● 如果有任何参与者返回 No:
○ 协调者发送 Abort 请求,所有参与者回滚事务。
这一阶段是 3PC 与 2PC 的关键区别:参与者已经进入一个“可以提交”的安全状态。
DoCommit 阶段(提交阶段)
● 如果 PreCommit 阶段所有参与者都确认成功:
○ 协调者向所有参与者发送 DoCommit 请求。
○ 参与者正式提交事务并释放资源。
● 如果有失败或超时:
○ 协调者发送 Abort 请求,参与者回滚事务。
如果协调者在这个阶段挂掉,参与者可以依赖超时机制自行提交事务,从而避免阻塞。
3PC为什么比2PC好?主要是解决了协调者和参与者都挂了的情况。
如果挂掉的那台机器已经执行了commit,那么协调者可以从所有未挂掉的参与者的状态中分析出来,并执行commit。如果挂掉的那个参与者执行了rollback,那么协调者和其他的参与者执行的肯定也是rollback操作。
所以,再多引入一个阶段之后,3PC解决了2PC中存在的那种由于协调者和参与者同时挂掉有可能导致的数据一致性问题。
2.3 什么是TCC
TCC是Try-Confirm-Cancel的缩写,它是一种分布式事务解决方案,采用了基于业务逻辑的补偿机制,将整个分布式事务分解为若干个子事务,每个子事务都有一个try、confirm和cancel三个操作,通过这些操作来实现分布式事务的执行和回滚。
具体来说,TCC事务包括以下三个步骤:
-
Try:在try阶段,参与者尝试执行本地事务,并对全局事务预留资源。如果try阶段执行成功,参与者会返回一个成功标识,否则会返回一个失败标识。
-
Confirm:如果所有参与者的try阶段都执行成功,则协调者通知所有参与者提交事务,那么就要执行confirm阶段,这时候参与者将在本地提交事务,并释放全局事务的资源。
-
Cancel:如果任何一个参与者在try阶段执行失败,则协调者通知所有参与者回滚事务。那么就要执行cancel阶段。
TCC这种事务方案有以下优缺点:
优点:
- 灵活性:TCC适用于不同类型的业务场景,例如账户转账、库存扣减等,能够根据业务逻辑实现精细的事务控制。
- 高可用性:TCC使用分布式锁来保证分布式事务的一致性,即使其中一个节点出现故障,也不会影响整个系统的运行。
- 可扩展性:TCC采用分阶段提交的方式,支持横向扩展,可以适应更多的并发访问和业务场景。
- 性能:TCC相对于2PC来说,具有更好的性能表现
缺点:
- 实现复杂:TCC需要实现Try、Confirm和Cancel三个操作,每个操作都需要实现正确的业务逻辑和补偿机制,代码实现比较复杂。
- 存在悬挂事务问题:TCC的实现方式存在悬挂事务的问题,即在执行过程中可能会有部分子事务成功,而其他子事务失败,导致整个事务无法回滚或提交。
- 空回滚问题:TCC中的Try过程中,有的参与者成功了,有的参与者失败了,这时候就需要所有参与者都执行Cancel,这时候,对于那些没有Try成功的参与者来说,本次回滚就是一次空回滚。需要在业务中做好对空回滚的识别和处理,否则就会出现异常报错的情况,甚至可能导致Cancel一直失败,最终导致整个分布式事务失败。
- 业务代码侵入性:TCC需要将事务操作拆分为Try、Confirm和Cancel三个步骤,对业务代码有一定的侵入性,需要针对不同的业务场景进行实现。
2.3.1 TCC的空回滚和悬挂是什么?如何解决?
在TCC中,存在着两个比较关键的问题,那就是空回滚和悬挂的问题。
- 空回滚问题:TCC中的Try过程中,有的参与者成功了,有的参与者失败了,这时候就需要所有参与者都执行Cancel,这时候,对于那些没有Try成功的参与者来说,本次回滚就是一次空回滚。需要在业务中做好对空回滚的识别和处理,否则就会出现异常报错的情况,甚至可能导致Cancel一直失败,最终导致整个分布式事务失败。
- 悬挂事务问题:TCC的实现方式存在悬挂事务的问题,在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况。举一个比较常见的具体场景:一次分布式事务,先发生了Try,但是因为有的节点失败,又发生了Cancel,而下游的某个节点因为网络延迟导致先接到了Cancel,在空回滚完成后,又接到了Try的请求,然后执行了,这就会导致这个节点的Try占用的资源无法释放,也没人会再来处理了,就会导致了事务悬挂。
这两个问题处理不好,都可能会导致一个分布式事务没办法保证最终一致性。有一个办法,可以一次性的解决以上两个问题,那就是——引入分布式事务记录表。
有了这张表,每一个参与者,都可以在本地事务执行的过程中,同时记录一次分布式事务的操作记录。
这张表中有两个关键的字段,一个是tx_id用于保存本次处理的事务ID,还有一个就是state,用于记录本次事务的执行状态。至于其他的字段,比如一些业务数据,执行时间、业务场景啥的,就自己想记录上就记录啥。
CREATE TABLE `distribute_transaction` (`tx_id` varchar(128) NOT NULL COMMENT '事务id',`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',PRIMARY KEY (`tx_id`) U
)
有了这张表以后,我们在做try、cancel和confirm操作之后,都需要在本地事务中创建或者修改这条记录。一条记录的状态机如下:
空回滚解决:当一个参与者接到一次Cancel请求的时候,先去distribute_transaction表中根据tx_id查询是否有try的记录,如果没有,则进行一次空回滚即可。并在distribute_transaction中创建一条记录,状态标记为cancel。
事务悬挂解决:当一个参与者接到一次Try请求的时候,先去distribute_transaction表中根据tx_id查询是否有记录,如果当前存在,并且记录的状态是cancel,则拒绝本次try请求或者直接返回try成功即可。
但是需要注意的是,上面的请求过程,需要做好并发控制。
有了这张表,我们还可以基于他做幂等控制,每次try-cancel-confirm请求来的时候,都可以到这张表中查一下,然后做幂等控制。
2.3.2 TCC中,Confirm或者Cancel失败了怎么办?
Confirm失败怎么办?
1、重试(用的最多)
一种常见的策略是重试Confirm操作。这通常适用于由于临时问题(如网络延迟、服务短暂不可用等)导致的失败。在重试之前,可以设定一个延迟或等待一段时间,然后再次尝试Confirm操作。通常,会设置重试次数的上限,以避免无限重试。
这个方案用的是最多的,之所以可以这么做,主要是因为在Try的过程中已经锁定了资源,那么在Confirm的时候,大概率是可以成功,而如果Confirm失败就执行Cancel,就会导致可能只是因为网络原因导致的时候就使得整个事务都Cancel了,而且这时候如果Cancel再失败怎么办呢?整个方案就会变得更加复杂了。
2、执行Cancel操作
如果重试Confirm操作依然失败,或者系统确定Confirm无法成功,下一步是执行Cancel操作。Cancel阶段的目的是撤销在Try阶段预留的所有资源,确保系统回到事务开始前的状态。这是一种典型的回滚操作,用于处理事务失败的情况。
3、日志记录和异常监控
在Confirm失败的情况下,重要的是记录详细的错误日志和监控异常。这可以帮助系统管理员或开发人员分析为什么Confirm操作失败,并采取相应的改进措施。此外,日志可以帮助在事后定位问题的根源。
4、人工干预
在某些复杂或重要的事务中,如果自动化的重试和回滚失败,可能需要人工干预。这涉及到系统管理员或运维团队直接介入,手动处理故障和确保系统的一致性与稳定性。
Cancel失败怎么办?
1、记录日志&发送报警:将错误信息记录下来,方便后续分析和处理。并及时通知相关人员进行处理。
2、自动重试:在一定程度上,可以通过自动重试的方式尝试多次执行Cancel操作,直到成功为止。
3、人工干预:如果重试多次还是不成功, 可以报警,然后进行人工干预,可以尝试手动执行Cancel操作或者进行数据修复等。
2.3.3 TCC和2PC有什么区别?
首先,二者的实现机制不同,2PC使用协调者和参与者的方式来实现分布式事务,而TCC采用分阶段提交的方式。
最大的区别:站在每一个事务的参与者角度看,TCC其实是把一个事务拆成了3个独立的事务,而2PC就只是一个事务。
● TCC中,把一次业务操作,拆分成Try、Confirm、Cancel三个步骤,每一个步骤操作数据库的时候都是一个单独的事务,单独开启,单独提交。
● 2PC是一个事务,拆分成了准备阶段和提交阶段,但是他还是一个事务,Prepare阶段事务是不提交的,直到第二个阶段实物才会提交。所以他的事务更长,阻塞时间更差,性能更差。
所以,你也就能理解,为啥2PC是一个强一致性的了,因为他是一个事务,是严格遵循ACID的。而TCC是最终一致性,通过全局锁和重试机制保证最终一致。
所以,2PC适用于对事务一致性要求较高的场景,例如银行转账等,需要保证数据一致性和完整性。而TCC适用于对事务一致性要求不那么高的场景,例如电商库存扣减等,需要保证数据最终一致性即可。
2.4 如何基于MQ实现分布式事务?
2.4.1 如何基于本地消息表实现分布式事务?
主要思想是将分布式事务拆分为本地事务和消息事务两个部分,本地事务在本地数据库中进行提交或回滚,而消息事务则将消息写入消息中间件中,以实现消息的可靠投递和顺序性。
一般来说的做法是,在发送消息之前,先创建一条本地消息,并且保证写本地业务数据的操作,和写本地消息记录的操作在同一个事务中。这样就能确保只要业务操作成功,本地消息一定可以写成功。
然后再基于本地消息,调用MQ发送远程消息。
消息发出去之后,等待消费者消费,在消费者端,接收到消息之后,做业务处理,处理成功后再修改本地消息表的状态。
这个过程中,可能有几个步骤都可能发生失败,那么如果失败了怎么办呢?
步骤1和步骤2如果失败,因为在同一个事务中,所以事务会回滚,3及以后的步骤都不会执行。数据是一致的。
步骤3如果失败,那么就需要有一个定时任务,不断的扫描本地消息数据,对于未成功的消息进行重新投递。
步骤4和步骤5如果失败,则依靠消息的重投机制,不断地重试。
步骤6和步骤7如果失败,那么就相当于两个分布式系统中的业务数据已经一致了,但是本地消息表的状态还是错的。这种情况也可以借助定时任务继续重投消息,让下游幂等消费再重新更改消息状态,或者本系统也可以通过定时任务去查询下游系统的状态,如果已经成功了,则直接推进消息状态即可。
优点:
- 可靠性高:基于本地消息表实现分布式事务,可以将本地消息的持久化和本地业务逻辑操作,放到一个事务中执行进行原子性的提交,从而保证了消息的可靠性。
- 可扩展性好:基于本地消息表实现分布式事务,可以将消息的发送和本地事务的执行分开处理,从而提高了系统的可扩展性。
- 适用范围广:基于本地消息表实现分布式事务,可以适用于多种不同的业务场景,可以满足不同业务场景下的需求。
缺点:
- 实现复杂度高:基于本地消息表实现分布式事务,需要设计复杂的事务协议和消息发送机制,并且需要进行相应的异常处理和补偿操作,因此实现复杂度较高。
- 系统性能受限:基于本地消息表实现分布式事务,需要将消息写入本地消息表,并且需要定时扫描本地消息表进行消息发送,因此对系统性能有一定影响。
- 回滚困难:本地消息表的方案,比较适合那种上游成功下游必须成功的场景,比如下单成功了,运费险必须成功。而对于可能需要整个事务都回滚的场景,不适合这个方案,或者说想要做就要很复杂的作回滚机制。
- 参与方多不适合:虽然消息可以发送给多个监听者,但是本地消息表中的数据只有一条,如果一个事务有多个参与方,这个方案不适合。虽然可以在消息表中增加多个状态字段,表示多个参与者的状态,但是就太耦合了。
- 会带来消息堆积扫表慢、集中式扫表会影响正常业务、定时扫表存在延迟问题等问题。在下文中介绍:
2.4.2 定时任务扫表的缺点有什么?
1、消息堆积,扫表慢
随着本地消息表中的数据量越来越大,通过定时任务扫表的方式会越来越慢,那么想要解决这个问题,首先可以考虑加索引。
我们可以在state字段上增加一个索引,虽然这个字段的区分度不高,但是一般来说,这张表中,SUCCESS的数据量占90%,而INIT的数据量只占10%,而我们扫表的时候只关心INIT即可,所以增加索引后,扫表的效率是可以大大提升的。
其次,可以考虑多线程并发扫表,这里可以考虑采用线程池,在任务中开多个线程并发的从数据库中扫描数据进行处理。
2、集中式扫表会影响正常业务
如果业务量比较大的话,集中式的扫描数据库势必给数据库带来一定的压力,那么就会影响到正常的业务。
那么想要解决这个问题,首先可以考虑,不扫主库,而是扫描备库。之所以能这么做,是因为这个业务场景一般都是可以接受一定的数据延迟的,那么备库带来延迟就可以忽略,但是备库是没有业务操作的,所以对备库的扫描是不会对业务造成影响的。
3、定时扫表存在延迟问题
定时任务都是集中式的定时执行的,那么就会存在延迟的问题。随着数据库越来越大,延时会越来越长。
想要降低延迟,那就要抛弃定时任务的方案,可以考虑延迟消息,基于延迟消息来做定时执行。
用了延迟消息之后,还可以缓解数据库的压力。也能比定时扫表的性能要好,实时性也更高。
2.4.3 为了避免丢消息问题需要落表,如何设计这张消息表?
以下是这张表需要包含的一些字段:
1、索引情况:
● state 作为普通索引,用于高效扫表。这个也可以和retry_count、next_retry_at等字段一起建一个联合索引。
● message_key +message_type 作为唯一索引,防止重复插入
2、消息必要信息
message_id这个是消息发送成功的时候才会有的一个信息,即MQ返回的,然后把他存下来,方便后续排查消息是否成功,以及对应的消息是否处理,以及后续如果要定位到具体的消息,也更加方便一点。
message_type这个表是一个消息的类型,比如是确认收货消息,还是发货消息,可以在扫表任务中,根据这个消息类型执行不同的消息处理逻辑。
3、状态信息
状态表示一个消息的处理状态,可以包括待发送,已发送,失败,已消费,已挂起。
其中待发送是初始状态,已失败和已消费是终止状态。
这里面的已失败状态,可以根据发送次数进行逻辑处理,比如设置一个阈值,比如发送10次,还是不成功,这是为啥已失败状态,避免重复处理。
针对这种多次不成功的消息,比如已失败状态的消息,根据实际的业务情况,可以考虑降低重试次数(比如待发送的消息10分钟重试一次,已失败的消息6小时重试一次),或者告警出来人工跟进。
2.4.4 什么是最大努力通知?
所谓最大努力通知,换句话说就是并不保证100%通知到。这种分布式事务的方案,通常也是借助异步消息进行通知的。
发送者将消息发送给消息队列,接收者从消息队列中消费消息。在这个过程中,如果出现了网络通信故障或者消息队列发生了故障,就有可能导致消息传递失败,即消息被丢失。因此,最大努力通知无法保证每个接收者都能成功接收到消息,但是可以尽最大努力去通知。
2.4.5 最大努力通知&事务消息&本地消息表三者区别是什么?
最大努力通知、MQ事务消息以及本地消息表都是依赖MQ实现最终一致性的方案。其中本地消息表和MQ事务消息其实是可靠消息最终一致性的两种具体实现。
这三者中,方案最简单的肯定是最大努力通知,因为他不需要保证消费者一定能接收到,只是尽自己最大的努力去通知就行了。最多就是在发消息的地方加一个重试的机制。这个方案缺点也很明显,那就是可能会导致消息的重复和丢失。
虽然最大努力通知也是一种最终一致性的方案,但是他的一致性保障并没有那么强,所以他不适合用在一些一致性要求较高的场景。只适合用在消息丢了也无所谓的场景。比如说下单后邮件通知、开通后发送欢迎短信之类的业务场景。
MQ事务消息以及本地消息表这两个方案一致性要求更高一些,但是同样方案也要更复杂一些。
MQ事务消息的方案首先要求这个MQ是支持事务消息的,其次对业务代码有侵入性,因为本来只需要发一次消息,用了这个方案之后需要改成发送两个half消息,并且同时还得给MQ提供一个反查的接口。
本地消息表这个方案对代码的侵入性没那么高,并且不需要MQ支持事务消息,但是他需要单独创建一张本地消息表,并且还需要提供一个定时任务来做轮询。所以他的改造成本也不低。
一般来说,事务消息和本地消息表比较适合于对一致性要求没那么高,不要求强一致,但是也不能丢的一些场景,比如用户下单后给用户增加积分。
一般来说,事务消息和本地消息表两个方案是可以互相替换的,用了事务消息的地方都可以换成本地消息表。但是实际来说,如果MQ支持事务消息,那么可以考虑这个方案,如果公司使用的MQ不支持事务消息,那么就可以考虑本地消息表。
2.4.6 什么是事务消息,为什么需要事务消息?
如果不用事务消息,就用本地消息的话,那么一次操作一般是这样的流程:
1、执行本地事务
2、发送MQ消息
3、消费MQ消息
如果一切顺利,那么没啥说的,双方都能处理成功,最终是一致的,但是实际情况是,因为网络延迟、网络抖动、服务器本身的稳定性、MQ自身的稳定性等原因,这个过程会出现各种各样的问题。
一旦在第一个参与者本地事务操作之后,如果出现了MQ发送失败、或者发送成功了,但是MQ自己存储失败了等原因,可能就会导致不一致了。
有人会问了,那这里如果MQ发失败了,本地事务回滚不就行了么?
有问题,因为会出现一种极端情况,那就是当出现网络抖动的时候,发送MQ因为网络超时返回了失败,本地事务回滚之后,但是网络超时不一定是MQ没有接收到,有可能处理成功了,但是返回的时候超时了。这时候就会出现:本地事务回滚了、但是MQ发送成功了的问题。这时候下游正常消费MQ之后,就又出现不一致了。
而且,MQ自身也不一定可靠,不管是哪种MQ,在极端情况下,都是有可能丢消息的,也就说,可能会出现本地事务成功之后,发送MQ成功了,但是因为MQ自身原因,导致消息丢了,还是会出现不一致。
所以,总之就是引入MQ之后,会因为各种原因导致不一致,那怎么解决这个问题呢?
解决方案就是能有一个机制保证MQ一定可以发送成功,或者是如果失败了,也有机制能够重试让他成功。
那么这个方案就是事务消息(RocketMQ中的那种,非Kafka中的那种),即把一个发送消息的过程拆成2步,先发一个半消息,确保成功之后,在执行本地事务,本地事务成功后,再发第二个半消息。
如果第一个半消息发失败了,本地事务不会执行。
如果第一个半消息发成功了,本地事务执行失败了,MQ也不会消费。
如果第二个半消息发送失败了,MQ会反查来决定commit还是rollback。
2.5 Seata的实现原理是什么
Seata是一个阿里开源的分布式事务解决方案(Simple Extensible Autonomous Transaction Architecture),用于在分布式系统中实现分布式事务。它旨在简化分布式事务的开发和管理,帮助解决分布式系统中的数据一致性问题。
因为Seata的开发者坚定地认为:一个分布式事务是有若干个本地事务组成的。所以他们给Seata体系的所有组件定义成了三种,分别是Transaction Coordinator、Transaction Manager和Resource Manager
Transaction Coordinator(TC): 这是一个独立的服务,是一个独立的 JVM 进程,里面不包含任何业务代码,它的主要职责:维护着整个事务的全局状态,负责通知 RM 执行回滚或提交;
Transaction Manager™: 在微服务架构中可对应为聚合服务,即将不同的微服务组合起来成一个完整的业务流程,TM 的职责是开启一个全局事务或者提交或回滚一个全局事务;
Resource Manager(RM):RM 在微服务框架中对应具体的某个微服务为事务的分支,RM 的职责是:执行每个事务分支的操作。
在一个下单事务中,我们有一个聚合的服务,姑且把他叫做TradeCenter吧,他负责接收并处理用户的下单请求,并且下单过程中需要调用订单服务(Order)、库存服务(Stock)及账户服务(Account)进行创建订单、扣减库存及增加积分。
所以TradeCenter担当的就是TM的角色,而Order、Stock及Account三个微服务就是RM的角色。在此之外,还需要一个独立的服务,维护分布式事务的全局状态,他就是TC。
因为TC维护着整个事务的全局状态,负责通知 RM 执行回滚或提交,所以他和TM、RM都是有交互的。并且TM和RM之间也有调用关系。多个RM之间可以是独立的。
上面这个场景中,要想保证分布式事务,就需要Order、Stock及Account三个服务对应的数据库表操作,要么都成功、要么都失败。不能有部分成功、部分失败的情况。
在用了Seata之后,一次分布式事务的大致流程如下(不同的模式略有不同,在介绍具体模式的时候分别展开):
1、TM在接收到用户的下单请求后,会先调用TC创建一个全局事务,并且从TC获取到他生成的XID。
2、TM开始通过RPC/Restful调用各个RM,调用过程中需要把XID同时传递过去。
3、RM通过其接收到的XID,将其所管理的资源且被该调用所使用到的资源注册为一个事务分支(Branch Transaction)
4、当该请求的调用链全部结束时,TM根据本次调用是否有失败的情况,如果所有调用都成功,则决议Commit,如果有超时或者失败,则决议Rollback。
5、TM将事务的决议结果通知TC,TC将协调所有RM进行事务的二阶段动作,该回滚回滚,该提交提交。
这里要求所有的RM都能做到2阶段,第一阶段做事务的预处理,第二阶段做事务的提交或者回滚。具体怎么实现,是否需要自己改代码,这个不同的模式不太一样。
2.5.1 Seata的4种事务模式,各自适合的场景是什么?
首先AT模式
,它的优点就是没有侵入性,你只需要按照Seata的要求引入@GlobalTransaction注解,就可以实现你的分布式事务了,他不需要做额外的操作,你只需要关注你的业务逻辑即可。
但是他的局限性就是只能支持那种具有ACID属性的关系型数据库的操作,比如MySQL,因为他要基于日志进行回滚。如果你的项目中,需要把写数据库和写Redis放到同一个分布式事务中,AT模式就不支持了。
其次是TCC模式
,这种模式可以支持多数据源的情况,不管你是Redis、MySQL还是ES,反正他就是要你自己实现Try、Confirm和Cancel,具体的逻辑你自己写,提交、回滚的代码你自己来实现就行了,所以他对代码有一定的侵入性。
还有就是Saga这种模式
,它适合长事务,什么是长事务呢?就是那种你有外部交互的场景,比如你要调微信支付,就可以用这种模式来管理这个分布式事务。
以上三种都是最终一致性,而XA模式
这种就适合于你对一致性要求非常高的场景,只有他是一种强一致性模型。
2.5.2 Seata的AT模式和XA有什么区别?
XA是一个典型的分布式事务解决方案,是一个强一致性模型,基于XA的2PC和3PC一直都是业内比较成熟的分布式方案,但是,他们都存在着各种各样的问题。
Seata中的AT模式其实和2PC很像,都是把一个分布式事务分成了2个阶段,那他们之间有什么区别呢?
主要区别就是在一阶段和二阶段做的事情上。
所以,他们的主要区别就在于一阶段是否直接提交事务,Seata的AT模式中,为了提升性能,直接提交了事务,在二阶段,需要回滚的话再执行回滚。而XA中,一阶段是只做资源占用,二阶段在进行回滚或者提交。
所以,AT的性能是明显高于XA的,因为在一阶段已经执行了实际的数据库操作,并不需要做资源的占用和锁定,而二阶段也只是在失败的情况下再执行回滚,所以性能相对较高。
而XA这种模式由于需要锁定资源直到二阶段结束,对数据库性能有较大影响,尤其在高并发环境下。
而在一致性方面,XA确实要比AT好的,因为在一阶段所有参与者都只进行预提交操作,二阶段再根据协调器的决定进行实际提交或回滚,确保了全局事务的一致性。
而AT这种模式,一阶段是做了数据提交的,提交后就可能被其他事物看到,那么就会出现事务被提前看到的情况。所以会存在短暂的不一致。
2.5.3 Seata的AT模式会不会出现脏读?为什么?
会出现一种脏读情况,只不过和传统脏读不一样,传统的脏读指的是读取到其他(MySQL本地事务)事务未提交的数据。而Seata会出现,读取到其他(分支事务)(本地事务)事务已提交但可能被全局回滚的数据。
AT模式的核心机制是两阶段提交:
● 第一阶段:本地事务立即提交,释放本地锁,数据对其他事务可见。
● 第二阶段:全局事务根据协调结果决定提交或回滚(通过undo log补偿)。
实会出现一种情况,那就是如果全局事务最终回滚,其他事务可能在P第一阶段结束后、第二阶段回滚前读取到已提交但即将被撤销的数据,导致逻辑上的脏读。
那么这个问题如何避免呢?
其实没啥好办法,因为事务已经提交了,没办法避免其他事务的读取,就算能实现,也会大大降低可用性。所以如果不能接受脏读,那么不要使用AT模式,可以选择其他的事务方案,比如TCC。
参考链接:
1、https://www.yuque.com/hollis666/wk6won/ywhzkoqti2n6zh6g
2、https://www.yuque.com/hollis666/wk6won/xhvbak3ouy6xqiml
3、https://www.yuque.com/hollis666/wk6won/cu01a1g1xxn2v52u
4、https://www.yuque.com/hollis666/wk6won/xm675quxo1bc5qm8
5、https://www.yuque.com/hollis666/wk6won/qro9fl9lsiinx1tu
6、https://www.yuque.com/hollis666/wk6won/pm1eeggifcc1vykp