现今互联网界,分布式系统和微服务架构盛行。业界著名的CAP理论也告诉咱们,在设计和实现一个分布式系统时,须要将数据一致性、系统可用性和分区容忍性放在一块儿考虑。html
一、CAP理论git
在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)3 个要素最多只能同时知足两个,不可兼得。其中,分区容忍性又是不可或缺的。github
举例:Cassandra、Dynamo 等,默认优先选择AP,弱化C;HBase、MongoDB 等,默认优先选择CP,弱化A。算法
二、BASE 理论shell
核心思想:数据库
数据的一致性模型能够分红如下 3 类:apache
分布式系统数据的强一致性、弱一致性和最终一致性能够经过Quorum NRW算法分析。网络
分布式事务的目的是保障分布式存储中数据一致性,而跨库事务会遇到各类不可控制的问题,如个别节点宕机,像单机事务同样的ACID是没法奢望的。架构
一、Two/Three Phase Commit并发
2PC,中文叫两阶段提交。在分布式系统中,每一个节点虽然能够知晓本身的操做时成功或者失败,却没法知道其余节点的操做的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,须要引入一个做为协调者的组件来统一掌控全部节点(称做参与者)的操做结果并最终指示这些节点是否要把操做结果进行真正的提交。 两阶段提交的算法以下:
第一阶段:
第二阶段:
两段提交最大的问题就是第3)项,若是第一阶段完成后,参与者在第二阶没有收到决策,那么数据结点会进入“不知所措”的状态,这个状态会block住整个事务。也就是说,协调者Coordinator对于事务的完成很是重要,Coordinator的可用性是个关键。
因些,咱们引入三段提交,三段提交在Wikipedia上的描述以下,他把二段提交的第一个段break成了两段:询问,而后再锁资源。最后真正提交。三段提交的核心理念是:在询问的时候并不锁定资源,除非全部人都赞成了,才开始锁资源。但三阶段提交也存在一些缺陷,要完全从协议层面避免数据不一致,能够采用Paxos或者Raft 算法。
目前两阶段提交、三阶段提交存在以下的局限性,并不适合在微服务架构体系下使用:
全部的操做必须是事务性资源(好比数据库、消息队列、EJB组件等),存在使用局限性(微服务架构下多数使用HTTP协议),比较适合传统的单体应用;
因为是强一致性,资源须要在事务内部等待,性能影响较大,吞吐率不高,不适合高并发与高性能的业务场景;
二、Try Confirm Cancel(TCC)
一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
Confirm:真正执行业务,不做任何业务检查;只使用Try阶段预留的业务资源;Confirm操做知足幂等性。
Cancel:释放Try阶段预留的业务资源;Cancel操做知足幂等性。
整个TCC业务分红两个阶段完成:
第一阶段:主业务服务分别调用全部从业务的try操做,并在活动管理器中登记全部从业务服务。当全部从业务服务的try操做都调用成功或者某个从业务服务的try操做失败,进入第二阶段。
第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操做。若是第一阶段全部try操做都成功,则活动管理器调用全部从业务活动的confirm操做。不然调用全部从业务服务的cancel操做。
与2PC比较:
缺点:
三、异步确保最终一致性
核心思想:
本地消息表
其基本的设计思想是将远程分布式事务拆分红一系列的本地事务。若是不考虑性能及设计优雅,借助关系型数据库中的表便可实现。
举个经典的跨行转帐的例子来描述。
第一步伪代码以下,扣款100,经过本地事务保证了凭证消息插入到消息表中:
begin transaction: update User set account = account - 100 where userId = 'A' insert into message(msgId, userId, amount, status) values('123','A', 100, 1) commit transaction
第二步,通知对方银行帐户上加100了。那问题来了,如何通知到对方呢?
一般采用两种方式:
两种方式其实各有利弊,仅仅依靠MQ,可能会出现通知失败的问题。而过于频繁的定时轮询,效率也不是最佳的(90%是无用功)。因此,咱们通常会把两种方式结合起来使用。
解决了通知的问题,又有新的问题了。万一这消息有重复被消费,往用户账号上多加了钱,那岂不是后果很严重?其实咱们能够消息消费方也经过一个“消费状态表”来记录消费状态。在执行“加款”操做以前,检测下该消息(提供标识)是否已经消费过,消费完成后,经过本地事务控制来更新这个“消费状态表”。这样子就避免重复消费的问题:
get msgId = '123'; check if mgsId is in message_applied(msgId); if not applied: begin transaction: update User set account = account + 100 where userId = 'B' insert into message_applied(msgId) values('123') commit transaction
上诉的方式是一种很是经典的实现,基本避免了分布式事务,实现了“最终一致性”。可是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库形成压力。因此,在真正的高并发场景下,该方案也会有瓶颈和限制的。
MQ(非事务消息)
一般状况下,在使用非事务消息支持的MQ产品时,咱们很难将业务操做与对MQ的操做放在一个本地事务域中管理。仍是以上述提到的“跨行转帐”为例,咱们很难保证在扣款完成以后对MQ投递消息的操做就必定能成功。这样一致性彷佛很难保证。
咱们来分析下可能的状况:
从上面分析的几种状况来看,貌似问题都不大的。那么咱们来分析下消费者端面临的问题:
如何保证消息与业务操做一致,不丢失?
主流的MQ产品都具备持久化消息的功能。若是消费者宕机或者消费失败,均可以执行重试机制的(有些MQ能够自定义重试次数)。
如何避免消息被重复消费形成的问题?
这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。若是MQ自身和业务都具备高可用性,理论上是能够知足大部分的业务场景的。不过在没有充分测试的状况下,不建议在交易业务中直接使用。
MQ(事务消息)
举个例子,Bob向Smith转帐,那咱们究竟是先发送消息,仍是先执行扣款操做?
好像均可能会出问题。若是先发消息,扣款操做失败,那么Smith的帐户里面会多出一笔钱。反过来,若是先执行扣款操做,后发送消息,那有可能扣款成功了可是消息没发出去,Smith收不到钱。除了上面介绍的经过异常捕获和回滚的方式外,还有没有其余的思路呢?
下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段经过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,若是确认消息发送失败了怎么办?RocketMQ会按期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱究竟是减了仍是没减呢?若是减了是回滚仍是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚仍是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。以下图:
各大知名的电商平台和互联网公司,几乎都是采用相似的设计思路来实现“最终一致性”的。这种方式适合的业务场景普遍,并且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,因此需二次开发,可参考RocketMQ的事务消息(transactional message)。
总结:
阅读了很多这方面的文章,在此基础上,总结一下分布式事务一致性的解决方案。分布式系统的事务一致性自己就是一个技术难题,目前没有一种很简单很完美的方案可以应对全部场景。分布式系统的一个难点就是由于“网络通讯的不可靠”,只能经过“确认机制”、“重试机制”、“补偿机制”等各方面来解决一些问题。在综合考虑可用性、性能、实现复杂度等各方面的状况上,比较好的选择是“异步确保最终一致性”,只是具体实现方式上有一些差别。
参考:
理性撕逼!分布式事务:不过是在一致性、吞吐量和复杂度之间,作一个选择
程立:《大规模SOA系统中的分布事务处事》