分布式系统事务一致性

一 分布式系统特色

现今互联网界,分布式系统和微服务架构盛行。业界著名的CAP理论也告诉咱们,在设计和实现一个分布式系统时,须要将数据一致性、系统可用性和分区容忍性放在一块儿考虑。html

一、CAP理论git

在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)3 个要素最多只能同时知足两个,不可兼得。其中,分区容忍性又是不可或缺的。github

  • 一致性:分布式环境下多个节点的数据是否强一致。
  • 可用性:分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果。
  • 分区容忍性:特指对网络分区的容忍性。

举例:Cassandra、Dynamo 等,默认优先选择AP,弱化C;HBase、MongoDB 等,默认优先选择CP,弱化A。算法

二、BASE 理论shell

核心思想:数据库

  • 基本可用(Basically Available):指分布式系统在出现故障时,容许损失部分的可用性来保证核心可用。
  • 软状态(Soft State):指容许分布式系统存在中间状态,该中间状态不会影响到系统的总体可用性。
  • 最终一致性(Eventual Consistency):指分布式系统中的全部副本数据通过必定时间后,最终可以达到一致的状态。

二 一致性模型

数据的一致性模型能够分红如下 3 类:apache

  1. 强一致性:数据更新成功后,任意时刻全部副本中的数据都是一致的,通常采用同步的方式实现。
  2. 弱一致性:数据更新成功后,系统不承诺当即能够读到最新写入的值,也不承诺具体多久以后能够读到。
  3. 最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺当即能够返回最新写入的值,可是保证最终会返回上一次更新操做的值。

分布式系统数据的强一致性、弱一致性和最终一致性能够经过Quorum NRW算法分析。网络

三 分布式事务

分布式事务的目的是保障分布式存储中数据一致性,而跨库事务会遇到各类不可控制的问题,如个别节点宕机,像单机事务同样的ACID是没法奢望的。架构

一、Two/Three Phase Commit并发

2PC,中文叫两阶段提交。在分布式系统中,每一个节点虽然能够知晓本身的操做时成功或者失败,却没法知道其余节点的操做的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,须要引入一个做为协调者的组件来统一掌控全部节点(称做参与者)的操做结果并最终指示这些节点是否要把操做结果进行真正的提交。 两阶段提交的算法以下:

第一阶段:

  1. 协调者会问全部的参与者结点,是否能够执行提交操做。
  2. 各个参与者开始事务执行的准备工做:如:为资源上锁,预留资源。
  3. 参与者响应协调者,若是事务的准备工做成功,则回应“能够提交”,不然回应“拒绝提交”。

第二阶段:

  • 若是全部的参与者都回应“能够提交”,那么,协调者向全部的参与者发送“正式提交”的命令。参与者完成正式提交,并释放全部资源,而后回应“完成”,协调者收集各结点的“完成”回应后结束这个Global Transaction。
  • 若是有一个参与者回应“拒绝提交”,那么,协调者向全部的参与者发送“回滚操做”,并释放全部资源,而后回应“回滚完成”,协调者收集各结点的“回滚”回应后,取消这个Global Transaction。

两段提交最大的问题就是第3)项,若是第一阶段完成后,参与者在第二阶没有收到决策,那么数据结点会进入“不知所措”的状态,这个状态会block住整个事务。也就是说,协调者Coordinator对于事务的完成很是重要,Coordinator的可用性是个关键。

因些,咱们引入三段提交,三段提交在Wikipedia上的描述以下,他把二段提交的第一个段break成了两段:询问,而后再锁资源。最后真正提交。三段提交的核心理念是:在询问的时候并不锁定资源,除非全部人都赞成了,才开始锁资源。但三阶段提交也存在一些缺陷,要完全从协议层面避免数据不一致,能够采用Paxos或者Raft 算法

目前两阶段提交、三阶段提交存在以下的局限性,并不适合在微服务架构体系下使用:

  • 全部的操做必须是事务性资源(好比数据库、消息队列、EJB组件等),存在使用局限性(微服务架构下多数使用HTTP协议),比较适合传统的单体应用;

  • 因为是强一致性,资源须要在事务内部等待,性能影响较大,吞吐率不高,不适合高并发与高性能的业务场景;

二、Try Confirm Cancel(TCC)

一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。

  1. Try:完成全部业务检查,预留必须业务资源。
  2. Confirm:真正执行业务,不做任何业务检查;只使用Try阶段预留的业务资源;Confirm操做知足幂等性。

  3. Cancel:释放Try阶段预留的业务资源;Cancel操做知足幂等性。

整个TCC业务分红两个阶段完成:

第一阶段:主业务服务分别调用全部从业务的try操做,并在活动管理器中登记全部从业务服务。当全部从业务服务的try操做都调用成功或者某个从业务服务的try操做失败,进入第二阶段。

第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操做。若是第一阶段全部try操做都成功,则活动管理器调用全部从业务活动的confirm操做。不然调用全部从业务服务的cancel操做。

与2PC比较:

  • 位于业务服务层而非资源层。
  • 没有单独的准备(prepare)阶段,Try操做兼备资源操做与准备能力。
  • Try操做能够灵活选择业务资源的锁定粒度。
  • 开发成本较高。

缺点:

  • Canfirm和Cancel的幂等性很难保证。
  • 这种方式缺点比较多,一般在复杂场景下是不推荐使用的,除非是很是简单的场景,很是容易提供回滚Cancel,并且依赖的服务也很是少的状况。
  • 这种实现方式会形成代码量庞大,耦合性高。并且很是有局限性,由于有不少的业务是没法很简单的实现回滚的,若是串行的服务不少,回滚的成本实在过高。

三、异步确保最终一致性

核心思想:

eBay 的架构师Dan Pritchett,曾在一篇解释BASE 原理的论文《 Base:An Acid Alternative》中提到一个eBay 分布式系统一致性问题的解决方案。它的核心思想是将须要分布式处理的任务经过消息或者日志的方式来异步执行,消息或日志能够存到本地文件、数据库或消息队列,再经过业务规则进行失败重试,它要求各服务的接口是幂等的。

本地消息表

其基本的设计思想是将远程分布式事务拆分红一系列的本地事务。若是不考虑性能及设计优雅,借助关系型数据库中的表便可实现。

举个经典的跨行转帐的例子来描述。

第一步伪代码以下,扣款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了。那问题来了,如何通知到对方呢?

一般采用两种方式:

  1. 采用时效性高的MQ,由对方订阅消息并监听,有消息时自动触发事件。
  2. 采用定时轮询扫描的方式,去检查消息表的数据。

两种方式其实各有利弊,仅仅依靠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投递消息的操做就必定能成功。这样一致性彷佛很难保证。

咱们来分析下可能的状况:

  1. 操做数据库成功,向MQ中投递消息也成功,皆大欢喜。
  2. 操做数据库失败,不会向MQ中投递消息了。
  3. 操做数据库成功,可是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操做将被回滚。

从上面分析的几种状况来看,貌似问题都不大的。那么咱们来分析下消费者端面临的问题:

  1. 消息出列后,消费者对应的业务操做要执行成功。若是业务执行失败,消息不能失效或者丢失。须要保证消息与业务操做一致。
  2. 尽可能避免消息重复消费。若是重复消费,也不能所以影响业务结果。

如何保证消息与业务操做一致,不丢失?

主流的MQ产品都具备持久化消息的功能。若是消费者宕机或者消费失败,均可以执行重试机制的(有些MQ能够自定义重试次数)。

如何避免消息被重复消费形成的问题?

  1. 保证消费者调用业务的服务接口的幂等性。
  2. 经过消费日志或者相似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)。

这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。若是MQ自身和业务都具备高可用性,理论上是能够知足大部分的业务场景的。不过在没有充分测试的状况下,不建议在交易业务中直接使用。

MQ(事务消息)

举个例子,Bob向Smith转帐,那咱们究竟是先发送消息,仍是先执行扣款操做?

好像均可能会出问题。若是先发消息,扣款操做失败,那么Smith的帐户里面会多出一笔钱。反过来,若是先执行扣款操做,后发送消息,那有可能扣款成功了可是消息没发出去,Smith收不到钱。除了上面介绍的经过异常捕获和回滚的方式外,还有没有其余的思路呢?

下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段经过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,若是确认消息发送失败了怎么办?RocketMQ会按期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱究竟是减了仍是没减呢?若是减了是回滚仍是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚仍是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。以下图:

各大知名的电商平台和互联网公司,几乎都是采用相似的设计思路来实现“最终一致性”的。这种方式适合的业务场景普遍,并且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,因此需二次开发,可参考RocketMQ的事务消息(transactional message)。

总结:

阅读了很多这方面的文章,在此基础上,总结一下分布式事务一致性的解决方案。分布式系统的事务一致性自己就是一个技术难题,目前没有一种很简单很完美的方案可以应对全部场景。分布式系统的一个难点就是由于“网络通讯的不可靠”,只能经过“确认机制”、“重试机制”、“补偿机制”等各方面来解决一些问题。在综合考虑可用性、性能、实现复杂度等各方面的状况上,比较好的选择是“异步确保最终一致性”,只是具体实现方式上有一些差别。

 

参考:

分布式系统的事务处理

分布式系统事务一致性解决方案

理性撕逼!分布式事务:不过是在一致性、吞吐量和复杂度之间,作一个选择

知乎:经常使用的分布式事务解决方案介绍有多少种?

一次给女友转帐引起我对分布式事务的思考

用消息队列和消息应用状态表来消除分布式事务

程立:《大规模SOA系统中的分布事务处事》

相关文章
相关标签/搜索