分布式事务有哪些常见的实现方案?
分布式事务有哪些常见的实现方案?
分布式常见的实现方案有 2PC、3PC、TCC、本地消息表、MQ消息事务、最大努力通知、SAGA事务 等等。
7.1 说说2PC两阶段提交?
说到2PC,就不得先说分布式事务中的 XA 协议。
在这个协议里,有三个角色:
- AP(Application):应用系统(服务)
- TM(Transaction Manager):事务管理器(全局事务管理)
- RM(Resource Manager):资源管理器(数据库)
XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。
两阶段提交的思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作还是回滚操作。
- 准备阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交
- 提交阶段:事务协调器要求每个数据库提交数据,或者回滚数据。
优点:尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。
缺点:
- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
7.2 3PC(三阶段提交)了解吗?
三阶段提交(3PC
)是二阶段提交(2PC
)的一种改进版本 ,为解决两阶段提交协议的单点故障和同步阻塞问题。
三阶段提交有这么三个阶段:CanCommit
,PreCommit
,DoCommit
三个阶段
- CanCommit:准备阶段。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- PreCommit:预提交阶段。协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务,参与者执行完操作之后返回ACK响应,同时开始等待最终指令。
- DoCommit:提交阶段。协调者根据参与者在准备阶段的响应判断是否执行事务还是中断事务:
- 如果所有参与者都返回正确的
ACK
响应,则提交事务 - 如果参与者有一个或多个参与者收到错误的
ACK
响应或者超时,则中断事务 - 如果参与者无法及时接收到来自协调者的提交或者中断事务请求时,在等待超时之后,会继续进行事务提交
可以看出,三阶段提交解决的只是两阶段提交中单体故障和同步阻塞的问题,因为加入了超时机制,这里的超时的机制作用于 预提交阶段 和 提交阶段。如果等待 预提交请求 超时,参与者直接回到准备阶段之前。如果等到提交请求超时,那参与者就会提交事务了。
无论是2PC还是3PC都不能保证分布式系统中的数据100%一致。
7.3 TCC了解吗?
TCC(Try Confirm Cancel) ,是两阶段提交的一个变种,针对每个操作,都需要有一个其对应的确认和取消操作,当操作成功时调用确认操作,当操作失败时调用取消操作,类似于二阶段提交,只不过是这里的提交和回滚是针对业务上的,所以基于TCC实现的分布式事务也可以看做是对业务的一种补偿机制。
- Try:尝试待执行的业务。订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1,然后将可用库存数量设置为库存剩余数量-1,。
- Confirm:确认执行业务,如果Try阶段执行成功,接着执行Confirm 阶段,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。
- Cancel:取消待执行的业务,如果Try阶段执行失败,执行Cancel 阶段,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
TCC 是业务层面的分布式事务,保证最终一致性,不会一直持有资源的锁。
- 优点: 把数据库层的二阶段提交交给应用层来实现,规避了数据库的 2PC 性能低下问题
- 缺点:TCC 的 Try、Confirm 和 Cancel 操作功能需业务提供,开发成本高。TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作
7.4 本地消息表了解吗?
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。
例如,可以在订单库新增一个消息表,将新增订单和新增消息放到一个事务里完成,然后通过轮询的方式去查询消息表,将消息推送到MQ,库存服务去消费MQ。
执行流程:
- 订单服务,添加一条订单和一条消息,在一个事务里提交
- 订单服务,使用定时任务轮询查询状态为未同步的消息表,发送到MQ,如果发送失败,就重试发送
- 库存服务,接收MQ消息,修改库存表,需要保证幂等操作
- 如果修改成功,调用rpc接口修改订单系统消息表的状态为已完成或者直接删除这条消息
- 如果修改失败,可以不做处理,等待重试
订单服务中的消息有可能由于业务问题会一直重复发送,所以为了避免这种情况可以记录一下发送次数,当达到次数限制之后报警,人工接入处理;库存服务需要保证幂等,避免同一条消息被多次消费造成数据不一致。
本地消息表这种方案实现了最终一致性,需要在业务系统里增加消息表,业务逻辑中多一次插入的DB操作,所以性能会有损耗,而且最终一致性的间隔主要有定时任务的间隔时间决定
7.5 MQ消息事务了解吗?
消息事务的原理是将两个事务通过消息中间件进行异步解耦。
订单服务执行自己的本地事务,并发送MQ消息,库存服务接收消息,执行自己的本地事务,乍一看,好像跟本地消息表的实现方案类似,只是省去 了对本地消息表的操作和轮询发送MQ的操作,但实际上两种方案的实现是不一样的。
消息事务一定要保证业务操作与消息发送的一致性,如果业务操作成功,这条消息也一定投递成功。
执行流程:
- 发送prepare消息到消息中间件
- 发送成功后,执行本地事务
- 如果事务执行成功,则commit,消息中间件将消息下发至消费端
- 如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
- 消费端接收到消息进行消费,如果消费失败,则不断重试
消息事务依赖于消息中间件的事务消息,例如我们熟悉的RocketMQ就支持事务消息(半消息),也就是只有收到发送方确定才会正常投递的消息。
这种方案也是实现了最终一致性,对比本地消息表实现方案,不需要再建消息表,对性能的损耗和业务的入侵更小。
7.6 最大努力通知了解吗?
最大努力通知相比实现会简单一些,适用于一些对最终一致性实时性要求没那么高的业务,比如支付通知,短信通知。
以支付通知为例,业务系统调用支付平台进行支付,支付平台进行支付,进行操作支付之后支付平台会去同步通知业务系统支付操作是否成功,如果不成功,会一直异步重试,但是会有一个最大通知次数,如果超过这个次数后还是通知失败,就不再通知,业务系统自行调用支付平台提供一个查询接口,供业务系统进行查询支付操作是否成功。
执行流程:
- 业务系统调用支付平台支付接口, 并在本地进行记录,支付状态为支付中
- 支付平台进行支付操作之后,无论成功还是失败,同步给业务系统一个结果通知
- 如果通知一直失败则根据重试规则异步进行重试,达到最大通知次数后,不再通知
- 支付平台提供查询订单支付操作结果接口
- 业务系统根据一定业务规则去支付平台查询支付结果