在现如今,各个公司的业务增长比较快,特别是做C端业务的公司,业务类的增长是非常快的。所以现在大家接触到的系统大多数都是一些微服务,分布式等系统。传统的单体IT架构不一定能满足现阶段的需求。随着业务的增长,数据量也会出现各种增长,那么单机的数据库架构也应该向分布式数据库的架构进行转变,那么我们的系统就会形成多套服务,多套数据库表,这时候为了保证业务的正常流程,各个服务之间就需要进行远程协助才能完成相关的事务操作。所以慢慢的就引入了分布式事务的概念,下面我们用案例来一步步的带大家了解下分布式事务。
一、案例演示
我们假设现在有一套电商系统,这套电商系统的业务比较复杂,用户流量比较多,在下单的场景的时候,我们需要做如下事情:
1、使用优惠券 2、创建一个新的订单 3、减去商品库存 4、用余额完成支付 5、把订单发送给物流系统 6、向用户发送消息
上面的流程我们模拟一个用户的下单流程,从使用优惠券到最后发送消息给用户的完整流程,我们可以看到上面的第一步到第四步我们是必须要同时完成的,要么全部失败,要么全部成功,但是呢,由于是电商系统,我们在建设的时候需要考虑系统的并发和分布式等特性,但是前期我们的用户和数据都比较少,所以暂时使用单体数据库,那这个时候的架构是:
然后慢慢的随着时间的推移,用户越来越多,数据量也越来越大,那么这时候我们就会涉及到分库分表了,那此时的架构就会演变成:
这时候的架构我们每一个模块就可能是一个单独的数据库了。但是上面的框架就会发生一个比较致命的问题,以用户下单为例,根据上面的流程,我们需要调用用户模块,商品模块,订单模块,支付网关模块等,这里每一个模块都对应到自己的数据库,每一个数据库只能管理当前对应的模块的数据事务。但是比如我们调用用户模块成功了,接着又去调用商品模块减库存,那这时候商品模块出现了错误,商品模块出现了事务回滚,但是用户模块是感知不到的,用户模块的数据就会被永久的存储,这样整个系统就出现了错误的数据,业务上也会出现各种千奇百怪的问题。怎么办呢?有没有什么办法可以有一个角色统一管理我这些服务呢,让这些服务有任何处理失败的情况,就自动发生回滚?
答案是有的,这就是分布式事务。
二、分布式事务是什么?
指⼀次⼤的操作由不同的⼩操作组成的,这些⼩的操作分布在不同的服务器上,分布式事务需要保证这些⼩操作要么全部成功,要么全部失败。从本质上来说,分布式事务就是为了保证不同数据库的数据⼀致性。
三、分布式事务的解决方案有哪些?
目前分布式事务的解决方案有以下几种:
序号 | 解决方案 | 分布式事务模型 |
1 | 两阶段提交 | 强一致性分布式事务 |
2 | 三阶段提交 | 强一致性分布式事务 |
3 | TCC | 最终一致性分布式事务 |
4 | 半消息模式 | 最终一致性分布式事务 |
3.1、二阶段提交(2pc)
两阶段提交顾名思义就是提交事务会发生两次,第一次属于预备准备的阶段,第二次是确认提交的阶段。即:PreCommit,DoCommit。举个例子:
业务:班主任准备带大家周六去春游爬山
第一阶段:班主任让大家准备春游出去的物品,大家都准备了,没问题。 这就代表准备阶段结束了,大家都可以去。
第二阶段:到了周六,班主任带大家一起去春游爬山了。这就代表的是确认提交。
在上面的例子里面,如果第一阶段张三说去不了,那么这个事务就不会发生第二阶段,会直接结束了,因为是准备条件没完全匹配。但是如果第一阶段大家都回答可以去,到了周六第二阶段,李四说去不了了,怎么办呢? 这就出现了bug,所以二阶段提交还是有一定的缺陷的。缺陷如下:
1、同步阻塞问题:执⾏过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三⽅节点访问公共资源不得不处于阻塞状态。 2、单点故障:由于(事务管理器)协调者的重要性,⼀旦协调者发⽣故障。(本地资源管理器)参与者会⼀直阻塞下去。尤其在第⼆阶段,协调者发⽣故障,那么所有的参与者还都处于锁定事务资源的状态中,⽽⽆法继续完成事务操作。(如果是协调者挂掉,可以重新选举⼀个协调者,但是⽆法解决因为协调者宕机导致的参与者处于阻塞状态的问题) 3、数据不⼀致:在⼆阶段提交的阶段⼆中,当协调者向参与者发送commit请求之后,发⽣了局部⽹络异常或者在发送commit请求过程中协调者发⽣了故障,这会导致只有⼀部分参与者接收到了commit请求。⽽在这部分参与者接到commit请求之后就会执⾏commit操作。但是其他部分未接到commit请求的机器⽆法执⾏事务提交。于是整个分布式系统便出现了数据不⼀致的现象。 4、⼆阶段⽆法解决的问题:参与者在发出commit消息之后宕机,⽽唯⼀接收到这条消息的协调者同时也宕机了。那么即使协调者通过选举协议产⽣了新的协调者,这条事务的状态也是不确定的,没⼈知道事务是否被已经提交了。
3.2、三阶段提交(3pc)
由于2阶段提交有比较大的缺陷,所以后来在二阶段提交的基础上又提出了新的概念,即三阶段提交,三阶段提交其实就是在二阶段提交的基础上增加了canCommit阶段,并且引入了超时机制。即:CanCommit,PreCommit,DoCommit。我们还是举个例子:
业务:班主任准备带大家周六去春游爬山
第一阶段:班主任问每一个同学周六准备去爬山去不去,大家都回答去。这时候班主任得到了大家的响应是yes,就代表周六可以去爬山,这就是CanCommit阶段。
第二阶段:班主任让大家准备春游出去的物品,大家都准备好了。这就是PreCommit阶段。
第三阶段:到了周六,班主任带大家一起去春游爬山了。这就是DoCommit阶段。
所以从概念来看:
CanCommit阶段:
它跟2PC的准备阶段很像,协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执⾏事务提交操作。然后开始等待参与者的响应 响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其⾃⾝认为可以顺利执⾏事务,则返回Yes响应,并进⼊预备状态。否则返回No
PreCommit阶段:
协调者根据参与者的响应情况来决定是否可以进⾏事务的PreCommit操作。根据响应情况,有以下两种可能: 假如协调者从所有的参与者获得的反馈都是Yes,那么就会执⾏事务的与执⾏。 发送预提交请求:协调者向参与者发送PreCommit请求,并进⼊Prepared阶段。 事务预提交:参与者接收到PreCommit请求后,会执⾏事务操作,并将undo和redo信息记录到事务⽇志中。 响应反馈:如果参与者成功的执⾏了事务操作,则返回ACK响应,同时开始等待最终指令。 假如有任何⼀个参与者向协调者发送了No响应,或者等待超时,或者协调者都没有接到参与者的响应,那么就执⾏事务的中断。 发送中断请求:协调者向所有参与者发送abort请求。 中断事务:参与者收到来⾃协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执⾏事务的中断。
DoCommit阶段:
该阶段进⾏真正的事务提交,也可以分为以下两种情况: 执⾏提交 发送提交请求:协调接收到参与者发送的ACK响应,那么将从预提交状态进⼊到提交状态。并向所有参与者发送doCommit请求。 事务提交:参与者接收到doCommit请求之后,执⾏正式的事务提交,并在完成事务提交之后释放所有事务资源。 响应反馈:事务提交完之后,向协调者发送ACK响应。 完成事务:协调者接收到所有参与者的ACK响应之后,完成事务。 中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执⾏中断事务。 发送中断请求:协调者向所有参与者发送abort请求 事务回滚:参与者接收到abort请求之后,利⽤其在阶段⼆记录的undo信息来执⾏事务的回滚操作,并在完成回滚之后释放所有的事务资源。 反馈结果:参与者完成事务回滚之后,像协调者发送ACK消息。 中断事务:协调者接收到参与者反馈的ACK消息之后,执⾏事务的中断。
3.3、TCC
TCC目前是一种比较成熟的分布式事务解决方案,也是用于解决跨库表操作数据一致性的方案。TCC的分布式解决方案物化到上面强一致性分布式事务模型里面,他属于二阶段提交模型。但是其整个框架是由Try,Confirm,Cancel 3个方法为主要实现。具体流程如下:
3.4、
还是举个例子来说:
业务:周六老师组织同学们去爬山。
在try的阶段,就是让大家准备好相关的装备,如果有失败,则调用cancel方法进行取消还原。
在confirm阶段,就是周六直接去爬山。在confirm阶段如果有失败,同样调用cancel方法进行取消还原。
在cancel阶段,就是把所有的状态全部重置还原。
我们在使用TCC的时候,我们还是需要注意以下几点:
1、业务操作分两个阶段完成
把资源的检查和预留放在一阶段的Try操作中进行,把真正的业务操作的执行放在二阶段的Confirm操作中进行,因为TCC服务要保证第一阶段Try操作成功之后,二阶段Confirm操作一定能成功;
2、允许空回滚
事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作 TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行
3、防悬挂控制
事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作; 在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况; 用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求;
4、幂等控制:
无论是网络数据包重传,还是异常事务的补偿执行,都会导致TCC服务的Try、Confirm或者Cancel操作被重复执行; 用户在实现TCC服务时,需要考虑幂等控制,即Try、Confirm、Cancel 执行次和执行多次的业务结果是一样的;
5、业务数据可见性控制
TCC服务的一阶段Try操作会做资源的预留,在二阶段操作执行之前,如果其他事务需要读取被预留的资源数据 那么处于中间状态的业务数据该如何向用户展示,需要业务在实现时考虑清楚; 通常的设计原则是“宁可不展示、少展示,也不多展示、错展示”
6、业务数据并发访问控制
TCC服务的一阶段Try操作预留资源之后,在二阶段操作执行之前,预留的资源都不会被释放;如果此时其他分布式事务修改这些业务资源,会出现分布式事务的并发问题; 用户在实现TCC服务时,需要考虑业务数据的并发控制,尽量将逻辑锁粒度降到最低,以最大限度的提高分布式事务的并发性;
3.4、半消息分布式事务
这里主要是以MQ的引入,实现可靠消息最终一致性及最大努力通知。这里主要以阿里巴巴开源的rocketmq为主要案例。具体流程如下:
1、发送方向RocketMQ服务端发送消息; 2、RocketMQ服务端将消息进行持久化,持久化成功之后,向发送方反馈ack确认消息,此时消息为半消息。这时候RocketMQ服务端存储的此条消息是不能被消费的。 3、发送方开始执行本地事务逻辑。 4、发送方根据本地事务执行结果向RocketMQ服务端提交二次确认(Commit或者是RollBack),RocketMQ服务端收到commmit状态,则将半消息标记为可投递, 这时候RocketMQ服务端存储的此条消息就可以被消费了。如果RocketMQ服务端收到的是rollback状态,则将此条消息给删除掉。持久层也就不再存储此条消息。 上诉流程就是与事务消息发送所对应的步骤。下面我们介绍下事务消息回查对应的步骤 5、在断网或者是应用重启等各种特殊情况下,上诉步骤4提交的二次确认最终没有到达RocketMQ服务端,那么RocketMQ服务端经过一段时间后将会对发送方发起消息回查的功能。 6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 7、发送方根据检查到的本地事务的最终状态再次提交二次确认,RocketMQ服务端继续按照上诉步骤4进行处理。
所以我们在使用半消息的时候,整个分布式事务几乎都是我们自己在手动的处理,我们需要在发送方实现如下功能:
1、负责发送对应的消息 2、负责执行本地事务,并且写事务日志表。 3、提供消息回查的接口 4、解决各种异常的处理
使用半消息事务这块目前根据我周边的了解,使用的还是比较少,因为里面涉及到非常繁杂的业务需要进行处理,写代码量非常大。一般我们建议使用TCC,因为很多框架这些都已经帮我们实现好了,我们只需要关系业务实现即可
还没有评论,来说两句吧...