分布式系统原理-分布式事务方案那么多,到底该选哪一个

 分布式系统原理系列目录

由于ACID只能保证单个数据源的一致性,跨系统(跨数据源)就没法保证了,为了保证跨系统的分布式事务必须确保原子提交,就有了分布式事务

XA

为了解决分布式事务的一致性问题,1991年的时候X/Open组织提出了一套叫做XA(eXtended Architecture)的事务处理标准。这个框架的核心内容是,定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务),以及他们之间的通讯标准。在实现层面,XA采用两阶段提交保证事务中的所有参与方统一提交或者统一回滚

如果应用程序要使用XA应用程序的技术栈要实现XA标准,包括数据库(常见的关系型数据库像mysql oracle db2都实现了XA),数据库驱动(JDBC),消息队列(activeMq),消息API(jms)

JTA

XA 不是 Java 规范,而是一套通用的技术规范。Java 后来专门定义了一套全局事务处理标准JTA(JSR 907 Java Transaction API)接口

JTA 原本是 Java EE 中的技术,一般情况下应该由 JBoss、WebSphere、WebLogic 这些 Java EE 容器来实现(很多老应用都是依赖于javaEE容器实现的),后来又出现了 JOTM(Java Open Transaction Manager),它以JAR包的形式实现了 JTA 的接口。这样子应用就不需要依赖于JBoss这种javaEE容器了,在Tomcat、Jetty 这样的 Java SE 环境下也可以使用 JTA 了

2pc/3pc,追求强一致性的事务机制

再来看看两阶段提交,2PC是解决原子提交问题最常见的办法,它是把事务提交拆分成了两个过程,也就是准备和提交两个阶段,所以称为两阶段

关于协议的细节网上资料很多,我这里就不多说了,我想强调下面几点:

12pc是基础设施层面的协议,就是上面说的准备、提交数据库、消息队列这种基础设施去实现,而不是应用程序去实现。我大概说下这两个阶段数据库是咋实现的,因为这个实现方式跟两阶段性能差很有关系:

准备阶段,也叫做投票阶段。在这一阶段,协调者询问事务的所有参与者是否准备好提交,如果已经准备好提交回复 Prepared,否则回复 Non-Prepared

这里的“准备”操作,对于数据库来说是干嘛呢,就是记录下redolog,但是不写入commit record,这是它和常规事务的区别。Commit record又称为事务的commit point(提交点),一旦写入了,就代表事务成功提交了,此后即使宕机也得保证数据被正常持久化。不写入Commit record就意味着事务还没有提交、而且还一直持有锁

提交阶段,协调者如果在准备阶段收到所有事务参与者回复的准备完成的消息,就会向参与者发送 Commit 指令,对于数据库来说,就是把刚刚没写的那条Commit Record写入下,就是个简短的追加写,所以这一步很快

23pc就是个扩展版的2pc,解决了2pc的两个问题。一个是协调者单点问题,2pc里提交阶段如果协调者跪了,参与者就会一直等,那事务就卡在那里了,锁也不会释放。3pc在参与者一侧引入了超时机制,参与者在最后一阶段如果没等到协调者的提交或者回滚消息,就直接提交。第二个是3pc降低了2pc回滚情况下的代价(或者说提升了性能),因为2pc的一阶段比较重(记了redo log日志,加了锁),如果要回滚,这一步很重的操作就全白做了

3pc2pc的第一个阶段又拆分成两个阶段,先询问,再做重量级的操作,如果询问阶段大家都返回OK,那成功的概率就很高了,然后再去做重量级操作,回滚的概率就很低了。但也3pc也引入了新的问题,

一个是他只是提高了回滚情况下的性能,但正常情况下性能变差了,因为多了一个阶段

还有一个是增加了不一致的风险,刚刚说的3pc在提交阶段,在参与者一侧引入了超时机制,参与者如果没等到协调者的提交或者回滚消息,就直接提交。那如果协调者最后发出的是回滚的指令,然后网络故障,有的参与者收到了,有的没收到,那就不一致了

最后总结一下,2pc3pc并不适用于微服务架构,几点原因:

1、因为他们是基础设施层面的协议,不是应用层的协议,很多应用用的nosql数据库,这些数据库很可能压根就不支持这个协议,比如mongoDBes,这样完全就没法玩了。

2、整个流程是同步执行的,所有参与者相当于被绑定成一个整体,整个过程要持续到参与者中最慢的哪一个处理完成才算完,因此性能比较差

32pc3pc是一种在分布式环境中仍追求强一致性的事务处理方案,这种事务称为刚性事务,这在微服务场景中是非常不合适的,我们在上一节BASE里也说了,分布式系统中可用性往往是高于强一致性的,大家更倾向于最终一致性的事务机制而不是强一致性的。从目前的情况来看,这种方案几乎只实际应用在了单服务多数据源的场景中

可靠消息队列/TCC/SAGA,追求最终一致性的事务机制

有刚性事务,就有柔性事务,柔性事务就不再追求强一致性了,而是追求最终一致性

可靠消息队列:实现简单,隔离性较差

第一种就是前面说的BASE这篇论文里提到的,作者先强调了分布式系统不应该追求强一致,而应该是最终一致,然后又提出了个最终一致性的分布式事务方案,叫做可靠消息队列

应用在自己的本地事务里,除了做业务操作,还会往数据库插入一条待完成的消息记录。后台会有一个专门管这个消息的进程,会扫描出待完成的记录,把消息通过消息队列发到下一个服务,下一个服务消费完消息后,回复一个已完成的消息,那个管消息的进程收到这个回复后, 把消息记录的状态改为已完成。这样就可以保证最终一致。这里有几个问题要注意下:

1、消息可能会重复发,比如下一个服务消费完了回复的消息丢了,这边就会重复发,这就要求服务要保证幂等

2管消息的那个进程有两种实现方式,一种就是轮询,实现很简单。一种是依赖于数据库binary log的增量订阅和消费,像canel这种工具,就是通过伪装成数据库的slave,接受binlog日志后发送消息到消息队列,避免了轮询的开销

3、只适用于第一个服务成功后,后面肯定都可以成功的情况,一次不成功一直重试到成功。不适用于那种第一个服务成功了,但是后面有可能会失败的场景

4、还有个比较大的问题是这种方案的隔离性比较差,第一个服务完成本地事务,到其他服务完成本地事务之间的时间跨度比较大,这段时间内整个分布式事务其实是处于一个中间状态的,而且客户端可以看到部分数据的,这就像我们本地一个事务可以看到另一个事务没提交的数据一样。有些系统不太看中这个隔离性,但是对有些系统来说,没有隔离性会比较麻烦

举个不是很贴切的例子,比如下单流程分为订单服务创建订单,然后调库存服务扣减库存。现在只剩一个库存,两个用户都发起下单请求,然后在订单服务这边都成功了,两个用户在页面看到自己的订单都成功了。然后订单服务发消息给库存服务让它扣减库存,其中一个请求在扣减时发现库存不足,要回滚订单,那之前有一个用户看到自己的订单成功了,这就属于脏数据

TCC(Try-Confirm-Cancel):性能好,适用于有强隔离要求的事务场景;业务侵入性强,不适用于涉及与外部系统的交互的场景

如果业务对隔离性要求比较高,我们就应该重点考虑 TCC 方案,它很适合用于需要强隔离性的分布式事务中,TCC由数据库专家帕特·赫兰德在2007年发表的论文《Life beyond Distributed TransactionsAn Apostate’s Opinion》中提出

在具体实现上,TCC 的操作有点麻烦,要求业务处理过程必须拆分为:预留业务资源-try、确认或者释放资源-也就是confirm或者cancel,这是业务代码实现的,侵入性比较强,不过也正因如此,他也比较灵活

预留资源和释放资源啥意思呢。还拿下单的场景举例,预留资源就是把库存冻结,比如说有个库存表,还要建个冻结库存表,冻结操作就是把库存表的库存减掉,在冻结库存表加上。如果最后事务提交,冻结库存表清理掉即可,如果要回滚就把冻结库存表的数据减掉,加到库存表去,这就是释放资源

通过这个预留资源的操作,其实就实现了隔离性,如果再发生并发下单的情况,第一步一个用户完成库存冻结后,另一个用户就会立即失败。而且这个预留的搞法不涉及锁,不会像2pc那样直接在数据库层面把记录锁住,所以性能会好很多。其实2pctcc的底层思想是一样的,只不过一个是数据库层面的,一个是应用层面的。TCC不用裸写,可以基于有些分布式事务中间件来搞,比如阿里开源的seata,能节省一些工作量,其他细节这里也不多说了

SAGA:内外部系统都适用,需要额外控制才能保证隔离性

刚刚说了,TCC 最主要的限制是它的业务侵入性很强,如果调用链路上都是公司内部的系统,通常都能搞定。但如果涉及外部系统,你让他配合你搞什么try/confirm/cancel改造,那他不一定会愿意,比如你要转账,你总不能要求银行给你开发个资金冻结的接口。这个时候,我们大概就只能考虑另外一种分布式事务方案SAGA

SAGA的历史比较悠久,由普林斯顿大学的赫克托·加西亚·莫利纳(Hector Garcia Molina)和肯尼斯·麦克米伦(Kenneth Salem)在1987年发表的论文《SAGAS》中提出,一开始还不是用于分布式事务的,解决的是如何防止大事务长时间锁住数据库的问题,后面逐渐演变成了分布式事务解决方案

SAGA说起来也很简单,每个参与事务的系统都要提供提交、撤销两个接口,撤销就是提交的逆操作,比如提交是给用户给商家转账,撤销就是商家给用户转账,而且都要保证幂等,因为可能会被重试。执行的时候,协调者依次调用提交接口,一旦有服务失败了,有两种策略,一种是一直重试直到它成功,然后接着往下,这种就是用于那种肯定要成功的场景,比如用户的钱扣了,就一定要给它充值成功。还有一种就是回滚,从当前服务按提交时的倒序往回撤销,撤销不成功就一直撤销,直到成功

这样子就不用要求人家有什么额外开发了。和TCC一样,SAGA也不用裸写,刚刚说的阿里的seata也支持SAGA方式的事务

其实SAGA的思想很简单,就是有个进程让所有参与方全部提交,如果提交失败就重试,直到成功为止,或者有人失败就让所有人都回滚。如果我们裸写代码去实现分布式事务,最有可能接近这种方案

SAGA说起来简单,但其实里面还是有不少细节的,比如谁来当协调者,某个服务来当,还是单独搞个协调者进程?整个过程同步还是异步?协调者有没有单点问题,比如要调abc三个服务,a提交后协调者挂了怎么办?实际使用时这些都是要考虑的问题

还有个就是我们之前说了隔离性,SAGA这个方案如果不加额外的控制,隔离性也是比较差的,还是比如要调abc三个服务,ab提交完了,提交的数据都已经被其他客户读到了,然后到c这边失败了,要回滚,那之前其他客户读到的数据就是脏读。如果想要隔离性,还要做很多工作,比如搞一些中间状态

所以我们说不存在完美的分布式事务方案,只有不断地trade off

不一定非要用分布式事务,对账也挺香的

好了,说了这么多种事务方案,那涉及到跨数据源的数据一致性问题,是不是非得要上分布式事务呢?肯定不是,对吧,事实上用分布式事务的系统挺少见。如果不用分布式事务,我们还有两种选择

一个是放弃一致性,比如你执行一个本地事务,然后再调外系统一个涉及到写数据的接口,如果就是裸调,是没办法保证百分百一致的,因为没办法hold外系统超时的情况。这种情况就是接受不一致的可能

还有一个是对账,日终时比一比两边数据是否一致,不一致就走补偿程序。对账是一种非常简单实用的机制,很多系统就是靠对账来保证最终一致性的,对他们来说,系统间的通讯失败概率很低,即使发生,最后还有对账这道防线

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值