杂谈分布式事务

标签: 分布式事务  事务  CAP  BASE

产生背景

随着业务规模发展,避免不了涉及到跨库、跨系统操作,这时候从集中式系统升级到分布式系统,分布式事务一致性问题由此产生。因此,在一致性要求较高的业务场景中,如何保证多个服务(系统)之间的数据一致性将成为关键点。

什么是分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

上面引自百度百科,简单来说就是一个业务操作包含多个子操作,并且子操作分属不同服务器上(也可以说操作不同数据库),这时候需要保证这些子操作要么全部成功,要么全部失败,进而达到一致性。

不得不说的几个概念

事务特性ACID

  • 原子性Atomicity

    整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性Consistency

    一致性表现为事务进行过后和执行前,整体系统都是稳定的,比如对于入账出账操作是不会有总资金的变化的。

    也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。其主要特征是保护性和不变性(Preserving an Invariant),以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。

  • 隔离性Isolation

    隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。

  • 持久性Durability

    所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此,并不会发生回滚。

CAP理论

  • 一致性Consistency

    在分布式环境下,一致性是指数据在多个副本之间能否保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。不要将弱一致性、最终一致性放到CAP理论里混为一谈,CAP理论强调的是强一致性,有人说能同时满足CAP理论,其实都是牺牲了C这个点。

    对于一个将数据副本分布在不同分布式节点上的系统来说,如果对第一个节点的数据进行了更新操作并且更新成功后,却没有使得第二个节点上的数据得到相应的更新,于是在对第二个节点的数据进行读取操作时,获取的依然是老数据(或称为脏数 据),这就是典型的分布式数据不一致的情况。在分布式系统中,如果能够做到针对一个数据项的更新操作执行成功后,所有的用户都可以读取到其最新的值,那么这样的系统就被认为具有强一致性。

  • 可用性Availability

    可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是”有限时间内”和”返回结果”。

    “有限时间内”是指,对于用户的一个操作请求,系统必须能够在指定的时间内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。另外,”有限的时间内”是指系统设计之初就设计好的运行指标,通常不同系统之间有很大的不同,无论如何,对于用户请求,系统必须存在一个合理的响应时间,否则用户便会对系统感到失望。

    “返回结果”是可用性的另一个非常重要的指标,它要求系统在完成对用户请求的处理后,返回一个正常的响应结果。正常的响应结果通常能够明确的反映出请求的处理结果,即成功或失败,而不是一个让用户感到困惑的返回结果。

  • 分区容错性Partition tolerance

    分布式系统在遇到任何网络分区故障的时候,仍然能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

分布式系统无法同时满足一致性、可用性、分区容错性三个特点,所以我们就需要根据业务场景做出选择。这里要强调的一点是:CAP定律的前提是P,当P决定后才有CA的抉择。因此,简单粗暴地说「三选二」是有一定误导性的。
CAP理论

选择 说明
CA 牺牲分区容错性,没有分区容错性怎么谈可用性,比如单机版的MySQL,Redis,MongoDB,节点挂了直接就没有可用性了。
CP 牺牲可用性,两个或者多个以上节点数据需要保证一致性的情况下,这时候节点之间出现网络故障,要么把这个节点标记为不可用(可用性降低),要么无限期的等待网络恢复保证一致性,还有一种就是服务降级不再提供写服务只提供读服务
AP 牺牲一致性,两个或者多个以上节点,节点之间出现网络故障的时候,为了高可用,这时候可惜牺牲强一致性。现在众多的NoSQL集群部署都属于此类。


BASE理论

BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:牺牲强一致性,根据自身业务特点,采用适当的方式来使系统达到最终一致性,从而提高可用性。接下来看一下BASE中的三要素:

  • 基本可用Basically Available
    指分布式系统在出现不可预知故障的时候,允许损失部分可用性,这绝不等价于系统不可用,比如服务降级、页面降级等等。
  • 软状态Soft state
    是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步存在延时。
  • 最终一致性Eventually consistent
    强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,其本质是需要系统保证数据最终能够达到一致,而不需要保证系统数据的实时强一致性。

ACID是传统数据库常用的设计理念,追求强一致性模型。BASE支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。ACID和BASE代表了两种相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此ACID和BASE又会结合使用。

几种分布式事务解决方案

事务消息最终一致性

前提条件是MQ必须支持事务消息,比如RocketMQ,原理有点类似于2PC。目前市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
MQ消息事务流程图
以阿里的RocketMQ中间件为例,其思路大致为:

  1. Prepared消息,拿到消息的地址;
  2. 执行本地事务;
  3. 通过第1步拿到的地址去访问消息,并修改状态。如果本地事务成功,则修改状态为已提交,否则修改状态为已回滚。

也就是说在业务方法内要向消息队列提交两次请求:一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口用来回调确认,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

优点
  1. 实现了最终一致性,不需要依赖本地数据库事务;
  2. 不占用业务DB资源;
  3. 没有耦合其它非业务代码。
缺点
  1. 主流开源MQ基本都不支持,自身实现难度大。

本地消息(事务)表

其核心设计思想是将分布式事务拆分成一系列的本地事务进行处理。其设计思路源于ebay经典的BASE方案。

举个例子,假设系统中有如下两个表:

coupon(id, code)
user_codex(id, user_id, code)

其中coupon记录券码信息,user_codex记录用户和券码关系。

begin;
    insert into coupon(id, $code);
    insert into user_codex(id, $user_id, $code);
commit;

如果2张表存储在不同的DB节点上,那么业务逻辑就是一个分布式事务操作,将其拆成2个本地事务,保证最终一致性。
在coupon表所在库中新增消息表biz_msg,这样就是在一个本地事务执行:

begin;
    insert into coupon(id, $code, $biz_id);
    insert into biz_msg(id, now(), $biz_id);
commit;

这里涉及的消息表数据没有其他用途,整个流程走完执行物理删除即可,像其他商品、交易等业务可能会涉及到状态,只对状态进行变更。

begin;
    insert into user_codex(id, $user_id, $code);
    delete from biz_msg where biz_id=$biz_id;
commit;

处理流程如下,暂时不讨论幂等:

  1. 生成券码插入记录到coupon表,成功后往本地消息表记录一条数据,其中包含业务唯一标识biz_id;
  2. 第一个事务处理成功之后,可以是方法调用(同一个系统),也可以是mq消息(不同系统)。第二个本地事务,首先插入记录到user_codex表,关联用户和券码。提交事务之后执行删除本地消息即可;
  3. 有一个定时任务,轮训扫描超过一定时间(gmt_create)还存在的消息表记录,重复执行第2个本地事务即可(需要业务保证可重入)。
优点
  1. 是一种非常经典且较简单的实现,避免了分布式事务,实现了最终一致性。
缺点
  1. 与具体的业务场景绑定,耦合性强,不可复用;
  2. 消息数据与业务数据同库,占用业务系统资源。

TCC补偿性事务

TCC其实采用的就是补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  1. Try阶段:主要是对业务系统做检测及资源预留;
  2. Confirm阶段:主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  3. Cancel阶段:主要是在try阶段不全部成功的情况下,需要对预留资源的进行释放。
    TCC事务一致性
优点
  1. 解决了跨应用业务操作的原子性问题,在诸如组合支付、账务拆分场景非常实用;
  2. 实际上把数据库层的二阶段提交上提到了应用层来实现,对于数据库来说是一阶段提交,规避了数据库层的2PC性能低下问题。
缺点
  1. Try、Confirm和Cancel操作功能需业务提供,开发成本高;
  2. Cancle要考虑部分cancle成功的场景,保证数据最终一致性。

最大努力通知型

是最简单的一种柔性事务,适用于一些最终一致性、时间敏感度低的业务,且被动方处理结果不影响主动方的处理结果,它本质上就是通过定期校对,实现数据一致性。典型的使用场景有:银行通知、商户通知等。

最大努力通知型的实现方案,一般符合以下特点:

  1. 不可靠消息:业务活动主动方,在业务处理完成之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
  2. 定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。
    最大努力通知型
优点
  1. 实现简单。
缺点
  1. 无补偿机制,不保证能够送达。

总结

分布式事务数据一致性,是没有一种完美的方案应对所有场景的,需要结合自身的业务需求、业务特点、技术架构以及各解决方案的特性,综合分析,才能找到最适合的方案。同时需要要做一些必要的监控告警,进行人工干预订正处理。

参考文章:
1. 关于分布式事务
2. 基于可靠消息方案的分布式事务:Lottor介绍
3. TCC两阶段补偿型
4. 核心金融场景分布式事务