你不知道的分布式事务
前言
面试官:看你简历,小伙子分布式挺了解呀,那你说说分布式事务
我:一脸无辜,别呀,我就写上去装个逼
不慌,小编今天就带大家了解下分布式事务的解决方案
正文
产生背景
随着公司的业务需求和体量的发展,传统的单体应用架构出现了瓶颈,满足不了现有的需求,加上微服务化架构的流行,原来的单体应用被拆分为一些列的微服务,例如拆分成了订单服务,用户服务,商品服务等等。每一个服务都分别使用独立的数据源。此时,每一个服务内的数据一致性仍由本地事务来保证。但是整个业务层面的全局数据一致性怎么保障呢?显然,本地事务是保证不了的。这就是微服务架构下面临的典型的分布式事务需求,我们需要一个分布式事务的解决方案来保障业务全局的数据一致性。
分类
对既有的分布式解决方案按照对业务侵入性可以分为两类:业务有侵入和业务无侵入。这里的侵入是指因为分布式这个技术问题的制约,而对应用在业务层面进行改造和设计。
业务无侵入:
- 基于XA的2PC
- 3PC
- 基于Seata的AT模式
业务有侵入:
- TCC方案
- 基于可靠的消息的最终一致性
- 本地消息表
- Soga事务模型
- 基于seata的TCC/Soga分布式解决方案
接下来就介绍下这些解决方案。
基于XA的2PC
很多同学肯定想问XA是什么? XA是一种协议,XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,主流数据库都提供了对XA的支持。 2PC是两段提交协议(Two Phase Commitment Protocol),它涉及两种角色:
- 事务协调者(coordinator): 负责协调多个参与者进行事务投票以及提交或者回滚
- 事务参与者(participants): 即本地事务的参与者,也是说是资源管理器
2PC处理步骤如下:
- 投票阶段(voting phase):协调者将通知事务的参与者准备提交事务,然后进入表决过程。事务参与者会告知协调者自己的决策:同意(参与者锁定资源,本地事务执行成功,但是处于未提交状态) ,取消(参与者本地事务执行失败);
- 提交阶段(commit phase): 收到事务参与者一阶段的决策后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交或是回滚,参与者收到协调者的请求后,执行commit或者rollback操作,然后释放事务资源,并向协调者反馈完成;
基于XA的2PC方案优点:
- 实现简单
- 业务无入侵
- 数据强一致
缺点:
- 要求数据提供对XA的支持,如果不支持XA协议或者支持的不好,则不能使用,如mysql5.7之前的版本
- 性能不高,不适合高并发场景。收协议本身的约束,事务资源的锁定周期长,要等两段提交完成,资源锁才会释放。资源管理器在数据本身,应用层无法插手。这样的问题是,基于XA的应用往往性能比较差,而且难以优化。
- 已经落地的基于 XA 的分布式解决方案,都依托于重量级的应用服务器(Tuxedo/WebLogic/WebSphere 等),这是不适用于微服务架构的。
- 单点问题,如果事务协调者挂了,参与者就会一直处于阻塞状态
- 存在数据不一致的问题,提交阶段如果由于网络延时等问题,导致部分参与者没有收到commit请求,这时就会出现数据不一致的问题
适用场景:
- 单体应用跨多个库的事务
3PC
三段提交协议是两段提交协议的改进版本,引入了超时机制解决了阻塞的问题,增加了询问阶段。
-
询问阶段: 事务协调者向所有事务参与者发送canCommit请求,询问参与者是否可以执行事务提交操作;参与者受到请求后,正常情况下,如果自身可以顺利执行事务,则返回Yes,否者就返回No
-
准备阶段:
- 如果询问阶段所有参与者返回的都是Yes,协调者向参与者发送preCommit,进入准备阶段,此时参与者受到请求后,执行事务操作,将Undo和Redo信息记入事务日志,并返回Ack响应,开始等待最终的指令;
- 如果询问阶段有任意一个参与者返回了No, 或者协调者等待超时之后没有收到参与者的请求,协调者就会向参与者发起abort中止请求。
-
提交阶段:
- 如果协调者收到了所有参与者Ack响应后,则会从预提交状态变为提交状态,并行所有参与者发送doCommit请求,事务参与者收到该请求后,执行正式的事务的提交,并且在完成事务之后对资源进行释放,然后想协调者发送ack响应;
- 如果协调者没有收到所有参与者的Ack响应(可能是响应超时,也可能是参与者发送本来就不是ack),则协调者会向所有参与者发送abort中止请求,事务参与者收到该请求之后,利用其在准备阶段记录的Undo信息来进行事务的回滚操作,回滚完成之后会释放事务资源,然后向协调者发送ack响应;
在提交阶段如果,如果参与者无法及时收到协调者发出的doCommit请求或者abort请求,会在等待超时之后继续执行事务的提交,当进入第三阶段,由于网络等原因导致参与者虽然没有收到协调者发送的doCommit请求或者abort请求,但是此时事务成功的几率很大,因为在第三阶段之前,协调者收到了所有参与者询问阶段的yes响应,并发送了preCommit请求,理论上此时事务成功的几率很大。
3PC 优缺点
优点:
- 解决2PC的单点问题和阻塞问题
缺点:
- 存在数据不一致的问题,提交阶段部分参与者如果没有收到abort请求,就会执行commit操作,而其他的参与者收到了abort请求,执行了回滚操作,这时又会出现数据不一致的问题
- 要求数据提供对XA的支持,如果不支持XA协议或者支持的不好,则不能使用,如mysql5.7之前的版本
- 性能不高,不适合高并发场景。收协议本身的约束,事务资源的锁定周期长,要等两段提交完成,资源锁才会释放。资源管理器在数据本身,应用层无法插手。这样的问题是,基于XA的应用往往性能比较差,而且难以优化。
TCC方案
TCC将事务提交分为Try - Confirm - Cancel3个操作,其实和两段提交类似,只是要在业务逻辑上来实现分布式事务,不依赖RM对分布式事务的支持。
- Try 对各个服务的资源做检测以及对资源进行锁定或者预留
- Confirm 确认执行业务操作,实际提交数据,不做任何业务检查,Trcc成功的话,confirm必然成功,需要保证幂等
- Cancel 取消业务操作,需要进行补偿,即实际回滚数据,需要保证幂等
我们以银行转账为例 有如下几个服务
转账服务(分布式事务发起方) BusinessService
转账订单服务(Orderservice)
扣钱服务(DecreaseService)
加钱服务(IncreaseService)
用户有两个字段,一个是可用余额,另外一个是冻结金额,Orderservice,DecreaseService,IncreaseService d都需要实现Try(), Comfirm() 以及Cancel()方法。
DecreaseService | IncreaseService | Orderservice | |
---|---|---|---|
Try() | 校验余额,冻结金额+100 余额-100 | 冻结余额+100 | 创建转账订单,待转账 |
Confirm() | 冻结金额-100 | 余额+100 冻结金额 -100 | 转账成功 |
Cancel() | 余额+100 冻结金额-100 | 冻结金额-100 | 转账失败 |
业务发起方 调用各个服务的try(), 如果所有的服务的try() 都执行成功,则对全局事务进行提交,即调用各自的confirm(); 如果,有任意一个服务的try() 抛出异常,事务管理器便会调用每个服务的cancel(),来对全局事务进行一个回滚。
使用TCC方案要注意一些问题,比如幂等性,空回滚,防悬挂等问题。
TCC方案优缺点
优点:
- 不需要数据库支持XA协议
- 性能好,锁粒度小
缺点:
- 需要入侵业务,实现比较复杂
- 复杂业务实现幂等有难度
适用场景:
- 交易、支付等场景
基于可靠的消息的最终一致性
基于可靠的消息可以利用RocketMq的事务消息来实现,本质上是两段提交的变种。我们需要RocketMq的两个概念:
- prepare消息 也称Half Message,标识该消息暂时不能投递,也就是不能被消费者消费, 只有broker收到生产者对该消息的commit或者rollback响应后,此消息才会被正常投递(被消费)或者回滚消息(不能被消费),它在commit或者rollback之前被存放在名为RMQ_SYS_TRANS_HALF_TOPIC的topic下,之后会存在名为RMQ_SYS_TRANS_OP_HALF_TOPIC的Topic
- 事务状态回查 当本地事务执行完了,如果producer 向mq发起消息确认请求,但是由于网路原因或者其他原因,导致mq没收到这个确认, 这时mq的 broker会定时扫描这些半消息,主动找producer确认这些消息,回查生产者的本地事务执行状态,来决定该半消息的回滚还是提交
该种方案也可分为二个阶段
-
第一阶段 生产者向RocketMq的 服务器broker发送事务消息,broker确认后回调生产者去执行本地事务
-
第二阶段 生产者执行完本地事务后(业务执行完成,同时将消息唯一标记与该业务执行记录同事入库,方便broker的事务回查),根据本地事务执行的结果,返回Commit/Rollback/Unknow 状态码 。 broker收到状态码后,如果收到的是commit,则将该消息置为可被消费,如果收到的是rollback,则会将该消息进行回滚,
如果收的状态是unknow,则等待broker定时发起状态回查,超过一定的重试次数,便会被丢弃
基于消息的的最终一致性的优缺点
优点:
- 降低业务系统的之前的耦合性
- 性能以及吞吐量优秀
缺点:
-
引入了消息中间件,增加了系统的复杂性
-
要实现事务回查listener
-
一次消息要发送两次网络请求
本地消息表
它的核心思想将分布式事务拆分成本地事务进行处理,会在各个业务数据库创建一张消息表。
核心过程:
- A系统在执行本地事务的同时会往消息表里插入一条数据
- 然后使用MQ的方式或者定时任务的方式 通知B系统
- B系统接受到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,若这个消息被处理过了,这个事务就会回滚,保证不会被重复消费
- B系统执行成功后就会更新本地消息表对应记录的状态 以及A系统的消息表的状态,如果B系统处理失败了,便不会更新消息表记录状态。A系统会定时扫描自己的消息表,如果有处理的消息,就会再次发送消息,让B系统再次消费。
如下图所示:
本地消息表的优缺点
优点:
- 消息数据的可靠性不依赖于消息中间件,弱化了对MQ的依赖
缺点:
- 与具体的场景业务绑定,耦合性强,不可公用
- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限
Soga事务模型
Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。它是一种分布式异步事务,实现的是最终一致性,是一种柔性事务,它有两种流行的实现方式。
事件/编排Choreography:没有中央协调器,也就没有单点问题风险,每个服务产生并聆听其他服务的事件,并决定是否应采取行动
实现的基本思想是:一个服务执行本地事务后,发布一个事件,这是会有一个或者多个参与者监听这个事件,这些服务受到这个事件后,执行本地事务并发布新的事件(或者不发布),当最后一个参与者执行完本地事务并不发布新的事件,此时标着着这个分布式事务的结束,或者它发布的事件没有被任何参与者监听到。
优点:
- 事件/编排是实现Saga模式的自然方式,很简单
- 事务参与者之前松散耦合
缺点
- 扩展性差
- 服务之间可能存在循环依赖
命令/协调orchestrator:中央协调器负责集中处理事件的决策和业务逻辑排序。
对比第一种方式,这种方式增加了协调者,以命令/回复的方式与每项服务进行通信,告诉事务的参与者们它们各自该怎么做。协调者事先必须要知道该事务的全部流程,如果期间事务要回滚,协调者会向每个参与者发送命令撤销之前的操作。
优点:
- 避免了服务之前的循环依赖
- 在添加新步骤时,事务复杂性保持线性,回滚更容易管理
- 集中分布式事务的编排
缺点:
- 容易产生单点问题,如果协调器故障,就会出现全局影响
**Soga模式适用场景:**步骤比较长的事务
好了今天就讲到这了,基于Seata的分布式事务解决方案会在后续详细介绍。