分布式事务原理解析及几种解决方案

1,308 阅读8分钟

一、本地事务与事务的特性

事务的概念对于我们开发来说,不是个陌生的东西,它用来控制一批业务操作一致完成不出错误,一个经典的例子就是银行转账,比如 A 要给 B 转100块钱,在整个转账过程里,涉及到以下几个步骤:

  1. 查询A账户余额值
  2. A 账户查出的余额值减100元
  3. A 账户减去100元的结果写回 A 账户余额字段上
  4. 查询 B 账户余额值
  5. B 账户查出的余额值增加100元
  6. B 账户增加100元的结果写入 B 账户余额字段上
public void transfer(BigDecimal money, User from_A ,User to_B){
    BigDecimal balanceOfA = getBalanceFromAcct(A);
    BigDecimal remindOfA = balanceOfA - money;
    setBalanceToAcct(A, remindOfA);
    BigDecimal balanceOfB = getBalanceFromAcct(B);
    BigDecimal remindOfB = balanceOfB + money;
    setBalanceToAcct(B, remindOfB);
}

我们都知道事务具有的4个基本属性:
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
这四个属性通常称为 ACID 特性。

我们平常操作转账,脑海里只会有一个转钱的动作的概念,不会想到一个转账操作,背后居然涉及到这么多细化的步骤,这是因为银行把整个转账流程封装到了一个事务中,这几个步骤也必须对外展现得就像只有一个操作一样,一气呵成,要么同时成功,要么同时失败,这就是事务的属性之一:原子性的体现。在整个操作中,绝不会发生只扣A的钱,而不增给B钱,也不会只给B账户加钱了,而不在A账户上扣除,转账操作开始前和操作发生后,所有人的账户上的钱总数是不变的。这就是事务的另一个特性的体现:一致性

事务的持久性体现在,事务操作后此次操作对于数据的修改就固定下来。在转账中即A给B的这笔100元转账确定到达B账户中并且归属于B所有了,B可以任意支配这100元进行买买买或者转给C、D、E、F...

隔离性涉及到不同事务间并发操作时互相之间的可见或者是对于共享的数据的访问权限等问题,这里暂时不展开解释。

如果上述转账操作发生在同一个数据库中,即一个本地事务,数据库本身所提供的事务支持就能完全保证操作的正确,而如果A和B的账户是属于不同银行的,我们要操作的账户分属于完全不同的数据库,我们的操作还能保证符合事务的所有条件吗?

二、分布式事务

随着微服务架构的发展和数据量的日益增大,分布式事务逐渐成为开发过程中不可避免的一个问题,分布式事务产生的场景可以归纳为以下几种:

跨库场景: 转账操作需要记录交易流水,数据量大时需要面临分库分表,一比交易的多条流水可能分布在不同的库中(水平拆分)

跨服务场景: 例如一个下单主流程,涉及到,扣减库存,新建订单,新增会员积分等服务, 这几个操作需要确保数据的一致性,扣减库存,订单,新增的会员积分要保持统一。

混合场景(既跨库又跨服务)

其实分布式事务归根结底就是针对不同数据库的操作过程中,如何去确保事务的基本ACID属性。

分布式理论中,一致性分为以下几种类型:弱一致性、强一致性、最终一致性。本地事务保证了数据的强一致性,分布式事务的一些解决方案则是围绕着这几种一致性展开。

三、分布式事务解决方案

2PC/3PC

两阶段提交协议(two phase commit)方案,简称 2PC,是分布式事务的核心协议,2PC 包含两个角色,协调者(Coornidator)和参与者(Cohort)还有两个主要阶段:
1、准备阶段(prepare)
协调者向参与者询问是否可以共同提交,获取全部参与者的的准备结果响应,然后进入提交阶段。

2、提交阶段(commit)
协调者根据准备阶段获取的所有参与者的准备信息向所有的参与者发布共同提交或者共同回滚的指令,用以保证事务达到一致性。

  • 2PC 优势:该协议能保证各个参与全局事务的节点的数据强一致性。
  • 2PC 存在问题:
    • 单点故障:协调者是整个流程的核心,如果协调者在提交阶段挂了,那么所有参与者就接受不到下一步操作的指令,进退两难,只能一直阻塞,影响很大。
    • 数据问题:提交阶段,如果因为网络问题,只有部分参与者接收到了提交命令,那么会造成节点间数据不一致问题。
    • 性能问题:参与者需要阻塞等待其他参与者响应接受指令,性能影响较大。

三阶段提交协议(three phase commit)方案,简称 3PC,是基于 2PC 协议的基础上的改进版本,为了解决 2PC 在提交阶段(commit)可能由于网络不通造成的数据不一致问题,在 2PC 的基础上,增加了preCommit阶段,使得在参与者在收不到确认时依然可以提交或者回退。

TCC

TCC(Try-Confirm-Cancel)是一种补偿型的事务模型,分为尝试(Try)、确认(Confirm)、取消(Cancel)三个阶段。 Try阶段:检测资源并对资源进行锁定或者预留。 Confirm阶段:执行事务。 Cancel阶段:释放资源或者如果发生异常在此阶段进行补偿或者回滚。

  • 优点:强一致性保证。
  • 缺点:需要自己实现补偿的逻辑,实现较复杂。

实现框架:tx-lcn www.txlcn.org

SAGA

SAGA 算法于1987年提出,是一种异步的分布式事务解决方案,其理论基础在于,假设所有事件按照顺序推进,总能达到系统的最终一致性,因此 SAGA 需要服务分别定义提交接口以及补偿接口,当某个事务分支失败时,调用其它的分支的补偿接口来进行回滚,SAGA 的具体实现分为两种:Choreography 以及 Orchestration。

  • 优点:降低了事务粒度,使得事务扩展更加容易,同时采用了异步化方式提升性能。
  • 缺点:在于很多时候很难定义补偿接口,回滚代价高,而且由于 SAGA 在执行过程中采用了先提交后补偿的思路进行操作,所以单个子事务在并发提交时的隔离性很难保证。

实现框架:Seata 阿里开源 github.com/seata/seata… seata.io/zh-cn/

事务消息

事务消息是一种异步确保型事务,将事务分支通过MQ进行异步解耦,本质上是对2PC的另一种形式实现。

可靠消息最终一致性方案

典型的实现就是RocketMQ对于事务的支持,我们来看下Rocket是如何实现可靠消息的最终一致性:

  1. 事务发起方给MQ发prepare消息。
  2. 事务发起方消息发送成功后执行本地事务。
  3. 通知MQ本地事务的执行结果,如果成功,通知MQ提交,如果失败,通知MQ删除prepare消息,本地事务回滚。
  4. MQ会自动轮询所有 prepared 消息回调事务发起者的接口询问事务执行状态。避免事务发起者在执行本地事务过程中成功,而在发送确认消息时出现问题。
  5. MQ的消费端的事务通过MQ自动不断重试来保证,消费端需要保证幂等性。

本地消息表

业务数据表和本地消息表在同一个数据库中,利用本地事务保证写业务数据和本地消息表数据的事务特性,并且使用消息队列来保证了最终一致性。 本地消息表的处理:

  1. 分布式事务操作的一方A执行业务操作;
  2. A向本地消息表写入消息,本地事务保证操作1和2的事务;
  3. 将本地消息表中的消息转发到MQ中,转发成功则将消息从本地消息表中删除,否则重试;
  4. 分布式事务操作的另一方B从消息队列中读取一个消息,并写入B所在服务数据库的本地消息表;
  5. B 系统执行业务操作,如果成功,则更新自己本地消息表的状态以及 A 系统消息表的状态;如果B的本地事务执行失败,则不更新消息表状态。
  6. A 系统定时扫描自己的消息表,如果有未处理的消息,会再次发送到 MQ 中去,MQ通知 B 再次处理;

最大努力通知

业务A本地事务执行完之后,发送消息到 MQ; 一个独立的最大努力通知服务会去消费MQ然后写入数据库中记录下来,接着调用业务B的接口;B执行成功,则操作完成;如果系统B执行失败,那么最大努力通知服务会定时尝试重新调用系统B进行重试。

参考链接: