分布式事务杂谈

什么是分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,
且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务的产生的原因

数据库分库分表

当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,
而且要保证数据的一致性,那么就要用到分布式事务。

微服务化

在微服务架构中,每个服务在用本地事务的时候,知道自己执行的事务是成功还是失败,
但是无法知道其他服务节点的事务执行情况,因此需要引入协调者TM,负责协调参与者RM的行为,并最终决定这些参与者是否把事务进行提交。

以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了!

事务的ACID特性

  • 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

  • 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

  • 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

  • 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

具体也可参考Mysql事务详解

分布式事务的应用场景

支付

最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,
要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,
对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。

在线下单

买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。

常见的分布式事务解决方案

刚性事务-强一致性

image

如上图,这是个标准的全局事务,事务管理器控制着全局事务,管理事务的生命周期,并通过XA协议与资源管理器协调资源;资源管理器负责控制和管理实际的资源
(这里的资源管理器,可以是一个DBMS,或者消息服务管理系统)

XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,
比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚

基于XA协议的两阶段提交(2PC)

它是XA用于在全局事务中协调多个资源的机制,常用于事务管理器和资源管理器之间,解决一致性问题,分两阶段:

image

第一阶段(提交事务请求)

image

应用程序调用了事务管理器的提交方法,此后第一阶段分为两个步骤:

  • 事务管理器通知参与该事务的各个资源管理器,通知他们开始准备事务。
  • 资源管理器接收到消息后开始准备阶段,写好事务日志并执行事务,但不提交,然后将是否就绪的消息返回给事务管理器(此时已经将事务的大部分事情做完,以后的内容耗时极小)。
第二阶段(执行事务请求)

image

第二阶段也分为两个步骤:

  • 事务管理器在接受各个消息后,开始分析,如果有任意其一失败,则发送回滚命令,否则发送提交命令。
  • 各个资源管理器接收到命令后,执行(耗时很少),并将提交消息返回给事务管理器。

事务管理器接受消息后,事务结束,应用程序继续执行。

为什么要分两步执行?

  • 一是因为分两步,就有了事务管理器统一管理的机会;
  • 二尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,
    这样,最后的提交阶段将是耗时极短,耗时极短意味着操作失败的可能性也就降低。

同时,二阶段提交协议为了保证事务的一致性,不管是事务管理器还是各个资源管理器,每执行一步操作,都会记录日志,为出现故障后的恢复准备依据。

基于XA协议的三阶段提交(3PC)

3PC最关键要解决的就是协调者和参与者同时挂掉的问题,所以3PC把2PC的准备阶段再次一分为二,
这样三阶段提交就有CanCommitPreCommitDoCommit三个阶段。在第一阶段,只是询问所有参与者是否可可以执行事务操作,并不在本阶段执行事务操作。当协调者收到所有的参与者都返回YES时,在第二阶段才执行事务操作,然后在第三阶段在执行commit或者rollback。

2PC VS 3PC

2PC的问题

二阶段提交协议的存在的弊端是阻塞,因为事务管理器要收集各个资源管理器的响应消息,如果其中一个或多个一直不返回消息,则事务管理器一直等待,应用程序也被阻塞,甚至可能永久阻塞。

  • 效率低,与本地事务相比,XA协议的系统开销比较大(数据被锁定的时间跨度整个事务,直到全局事务的结束),只有支持XA协议的资源才能参与分布式事务。
  • 2PC是反可伸缩模式的,在事务处理过程中,参与者需要一直持有资源直到整个事务的结束,这样当业务规模越来越大的情况下,它的局限性就越明显。
  • 数据不一致,在2pc中的第二阶段时,当TM向RM发送提交请求之后,发生局部的网络异常或者在发送提交请求过程中TM发生故障, 这会导致只有一部分RM收到了提交请求,然后没有收到提交请求的RM不会执行事务的提交,于是整个分布式系统便会出现数据不一致。
  • 单点故障, 由于TM的重要性,一旦发生故障,整个事务失效
3PC的改进
  • 3PC解决了2PC中存在的那种由于协调者和参与者同时挂掉有可能导致的数据一致性问题

  • 增加了超时机制, 主要解决单点故障问题,并减少资源锁定时间,
    一旦RM无法及时收到来至TM的信息之后,它会默认执行Commit操作, 而不会一直持有事务资源并处于阻塞状态。

3PC的问题

但是这种机制同样会导致数据不一致的问题,
由于网络的原因,TM发送的回滚动作,没有被RM及时的收到,那么RM等待超时后就执行了提交操作,
这样就和收到回滚操作并执行的RM之间存在了数据不一致的情况。

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。

所以,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

详见 分布式事务中2PC与3PC的区别

柔性事务 - 最终一致性

在2008年,eBay公布了基于BASE准则的最终一致性解决方案,它主要采用了消息队列来辅助实现事务控制流程,其核心通过消息队列的方式来异步执行分布式处理的任务,如果事务失败,则可以发起人工重试的纠正流程(比如对账系统,对处于dead letter queue的问题进行处理)

  • 消息发送一致性

微服务架构下,需要通过网络进行通信,就自然引入了数据传输的不确定性,也就是CAP原理中的P-分区容错,而这里的消息发送一致性是可靠消息的保证。

生成消息的业务动作与消息发送的一致(e.g: 如果业务操作成功,那么由这个业务操作所产生的消息一定会成功投递出去,否则就丢失消息)

image

如上图,保证消息发送一致性的一般流程如下:

  • Producer先把消息发送给消息中间件服务,消息的状态标记为待确认,这个状态并不会被Consumer消费,对于长期待确认的消息,消息中间件会调用Producer的查询接口,查看最新状态,根据结果决定是否删除消息。
  • Producer执行完业务操作后,向消息中间件服务,发送确认消息
    这时消息的状态会被更改为待发送(可发送)
  • Consumer监听并接收待发送状态的消息,执行业务处理
  • Consumer业务处理后,向消息中间件服务发送ACK,确认消息已经收到(消息中间件服务将从队列中删除该消息)

消息的ACK确认流程中,任何一个环节都可能会出问题!

对未ACK的消息,采用按规则重新投递的方式进行处理(很多MQ都提供at least once的投递,持久化和重试机制),一般还会设置重发的次数, 超过次数的消息会进入dead letter queue,等待人工干预或者延后定时处理。

  • 业务接口的幂等性

消息的重复发送会导致业务接口出现重复调用的问题,主要原因就是消息没有及时收到ACK确认导致的, 那如何实现幂等性设计呢?

在实际的业务场景中, 业务接口的幂等性设计,常结合查询操作一起使用,

比如根据唯一标识查询消息是否被处理过, 或者根据消费日志表,来维护消息消费的记录。

保证最终一致性的模式

  • 可查询模式,任何一个服务操作都提供一个可查询接口,用来向外部输出操作执行的状态,下游Consumer可以通过接口得知服务操作执行的状态,然后根据不同的状态做不同的处理操作(执行或者取消), 该模式对业务接口有一定侵入性。
  • 补偿模式, 有了查询模式,我们能够知道操作的具体状态,如果处于不正常状态,我们可以修正操作中出现的问题,或许是重新执行,或许取消已经完成的操作,通过修复是的整个分布式系统达到最终一致。
  • 最大努力通知模式, 在调用支付宝交易接口或微信支付接口时,一般会在回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。

所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下

image

  • 1、A系统向消息中间件发送一条预备消息
  • 2、消息中间件保存预备消息并返回成功
  • 3、A执行本地事务
  • 4、A发送提交消息给消息中间件

通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:

  • 步骤一出错,则整个事务失败,不会执行A的本地操作
  • 步骤二出错,则整个事务失败,不会执行A的本地操作
  • 步骤三出错,这时候需要回滚预备消息,怎么回滚?
    答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息
  • 步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务

基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:

image

虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。

TCC编程模式

所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。

详见 TCC分布式事务的实现原理!

总结

分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力!

参考

最终一致性分布式事务如何保障实际生产中99.99%高可用?

TCC分布式事务的实现原理!

微服务架构:最终一致性 + 事务补偿

分布式事务XA

XA两阶段提交协议

分布式事务中2PC与3PC的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值