博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
分布式-事务
阅读量:2181 次
发布时间:2019-05-01

本文共 23965 字,大约阅读时间需要 79 分钟。

分布式-事务

转载声明

本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:

  • 作者:58神剑
    出处:架构师支路

  • 作者:58神剑
    出处:架构师支路

  • 作者:bluishglc
    出处:CSDN

  • 作者:追着蜗牛打
    出处:CSDN

  • 作者: 陈彩华
    出处:51CTO技术栈

  • 作者:小小程序猿
    出处:知乎

  • 作者: 无敌码农

  • 作者: 无敌码农

1 事务简介

1.1 事务意义

  • 多个数据要同时操作,如何保证数据的完整性,以及一致性?

    答:事务,是常见的做法。事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。
    事务

  • 举个栗子:

    单数据源事务
    用户下了一个订单,需要修改余额表,订单表,流水表,于是会有类似的伪代码:

start transaction; CURD table t_account;  any Exception rollback; CURD table t_order;      any Exception rollback; CURD table t_flow;        any Exception rollback;commit;
  • 如果对余额表,订单表,流水表的SQL操作全部成功,则全部提交
  • 如果任何一个出现问题,则全部回滚

事务,可保证数据的完整性以及一致性。

1.2 事务问题

互联网的业务特点,数据量较大,并发量较大,经常使用拆库的方式提升系统的性能。如果进行了拆库,余额、订单、流水可能分布在不同的数据库上,甚至不同的数据库实例上,此时就不能用数据库原生事务来保证数据的一致性了。

1.3 数据库事务的 ACID 属性

1.3.1 简介

事务是基于数据进行操作,需要保证事务的数据通常存储在数据库中,所以介绍到事务,就不得不介绍数据库事务的 ACID 特性。

ACID 指数据库事务正确执行的四个基本特性的缩写,包含以下四个:

ACID

1.3.2 原子性(Atomicity)

整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。

如果事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

例如:银行转账,从 A 账户转 100 元至 B 账户,分为两个步骤:

  1. 从 A 账户取 100 元。
  2. 存入 100 元至 B 账户。

这两步要么都完成,要么都不完成。因为如果只完成第一步,第二步失败,钱会莫名其妙少了 100 元。

1.3.3 一致性(Consistency)

指在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏。

以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。

1.3.4 隔离性(Isolation)

数据库允许多个并发事务同时对数据进行读写和修改的能力,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。

隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

例如:现有有个交易是从 A 账户转 100 元至 B 账户,在这个交易事务还未完成的情况下,如果此时 B 查询自己的账户,是看不到新增加的 100 元的。

1.3.5 持久性(Durability)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

1.3.6 小结

简单而言,ACID 是从不同维度描述事务的特性:

  • 原子性:事务操作的整体性。
  • 一致性:事务操作下数据的正确性。
  • 隔离性:事务并发操作下数据的正确性。
  • 持久性:事务对数据修改的可靠性。

2 补偿事务(TCC)

2.1 简介

补偿事务,是一种在业务端实施业务逆向操作事务。TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现。

2.1.1 Try 阶段

主要是对业务系统资源做检测及预留、

TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:

  • 完成所有业务检查( 一致性 ) 。
  • 预留必须业务资源( 准隔离性 ) 。
  • Try 尝试执行业务。

TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。

因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。

2.1.2 Confirm 阶段

主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,当 Try 阶段服务全部正常执行,Confirm执行确认业务逻辑操作。

Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。

2.1.3 Cancel

当 Try 阶段存在服务执行失败, 进入 Cancel 阶段。

主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

2.1.4 小结

TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。

2.2 例子

  • 修改余额,事务为:
int Do_AccountT(uid, money){
start transaction; //余额改变money这么多 CURD table t_account with money for uid; anyException rollback return NO; commit; return YES;}

那么,针对修改余额的补偿事务可以是:

int Compensate_AccountT(uid, money){
//做一个money的反向操作 return Do_AccountT(uid, -1*money){
}

同理,订单操作,事务是:Do_OrderT,新增一个订单;

订单操作,补偿事务是:Compensate_OrderT,删除一个订单。

要保证余额与订单的一致性,伪代码:

// 执行第一个事务int flag = Do_AccountT();if(flag=YES){
//第一个事务成功,则执行第二个事务 flag= Do_OrderT(); if(flag=YES){
// 第二个事务成功,则成功 return YES; } else{
// 第二个事务失败,执行第一个事务的补偿事务 Compensate_AccountT(); }}
  • TCC例子

    假入 Bob 要向 Smith 转账,思路大概是:
    我们有一个本地方法,里面依次调用

    1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
    2. 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
    3. 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

    缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

  • TCC例子2

    以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。

    1. try

      假设商品库存为 100,购买数量为 2。这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
      try

    2. Confirm 阶段

      当 Try 阶段服务全部正常执行, 执行Confirm业务逻辑操作

      Confirm 阶段使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。

      Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。

      confirm

    3. Cancel

      当 Try 阶段存在服务执行失败, 进入 Cancel 阶段
      Cancel
      Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。

2.3 补偿事务小结

2.3.1 优点

TCC 事务机制相比于XA 事务机制(2PC,3PC),有以下优点:

  • 性能提升
    具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
  • 数据最终一致性
    基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据一致性。
  • 可靠性
    解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

2.3.2 缺点

  • 没有考虑补偿事务的失败。在2,3步中都有可能失败。
  • 补偿实现复杂
    TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。具体来说, TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
  • 不同的业务要写不同的补偿事务,不具备通用性;
  • 如果业务流程很复杂,if/else会嵌套非常多层,成指数型上升;

画外音:上面的例子还只考虑了余额+订单的一致性,就有22=4个分支,如果要考虑余额+订单+流水的一致性,则会有222=8个if/else分支,复杂性呈指数级增长。*

2.4 多个事务情况

2.4.1 多个事务带来的不一致

单库是用这样一个大事务保证一致性:

start transaction; CURD table t_account;  any Exception rollback; CURD table t_order;      any Exception rollback; CURD table t_flow;        any Exception rollback;commit;

拆分成了多个库后,大事务会变成类似以下的三个小事务,发生在三个库甚至三个不同实例的数据库上:

多库

start transaction1;         //第一个库事务执行         CURD table t_account; any Exception rollback;         …// 第一个库事务提交commit1;

一个事务,分成执行与提交两个阶段:

执行(CURD)的时间很长提交(commit)的执行很快

于是三个事务的整个执行过程的时间轴如下:

三个事务

注意,可能出现数据不一致:

第一个事务成功提交之后,最后一个事务成功提交之前,如果出现问题(例如服务器重启,数据库异常等),都可能导致数据不一致。
数据不一致

2.4.2 后置提交优化保证一致性

如果改变事务执行与提交的时序,变成事务先执行,最后一起提交,就是后置提交,如下:

后置提交

  • 后置提交优化后,在什么时候,会出现不一致?
    答:问题的答案与之前相同,第一个事务成功提交之后,最后一个事务成功提交之前,如果出现问题(例如服务器重启,数据库异常等),都可能导致数据不一致。
    后置提交不一致

2.4.3 串行事务和后置提交差异

答:

  • 串行事务方案,总执行时间是303ms,最后202ms内出现异常都可能导致不一致;

  • 后置提交优化方案,总执行时间也是303ms,但最后2ms内出现异常才会导致不一致;

    虽然没有彻底解决数据的一致性问题,但不一致出现的概率大大降低了。

2.4.4 后置提交不足?

答:对事务吞吐量会有影响:

  • 无法彻底解决多个事务数据不一致性风险

  • 串行事务方案,第一个库事务提交,数据库连接就释放了;

    而后置提交优化方案,所有库的连接,要等到所有事务执行完才释放;这就意味着,数据库连接占用的时间增长了,系统整体的吞吐量降低了。

2.5 小结

分布式事务,两种常见的实践:

  • 补偿事务
  • 后置提交优化

trx1.exec(); trx1.commit();trx2.exec(); trx2.commit();trx3.exec(); trx3.commit();

优化为:

trx1.exec(); trx2.exec(); trx3.exec();trx1.commit(); trx2.commit(); trx3.commit();

这个小小的改动(改动成本极低),不能彻底解决多库分布式事务数据一致性问题,但能大大降低数据不一致的概率,牺牲的是吞吐量。

对于一致性与吞吐量的折衷,还需要业务架构师谨慎权衡折衷。

3 分布式事务

3.1 基础概念

3.1.1 概述

随着互联网快速发展,微服务,SOA 等服务架构模式正在被大规模的使用,现在分布式系统一般由多个独立的子系统组成,多个子系统通过网络通信互相协作配合完成各个功能。具体来说,在分布式环境下,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。

当一个分布式事务跨多个节点时,保持事务的原子性与一致性,是非常困难的。我们需要一个分布式事务的解决方案保障业务全局的数据一致性。

有很多用例会跨多个子系统才能完成,比较典型的是电子商务网站的下单支付流程,至少会涉及交易系统和支付系统。而且这个过程中会涉及到事务的概念,即保证交易系统和支付系统的数据一致性,此处我们称这种跨系统的事务为分布式事务。

分布式事务

3.1.2 交易系统分布式事务场景

交易系统

上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。

在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。

交易系统2
可以看到,如果多个数据库之间的数据更新没有保证事务,将会导致出现子系统数据不一致,业务出现问题。

3.1.3 分布式事务的难点

  • 事务的原子性

    事务操作跨不同节点时,当多个节点某一节点操作失败时,需要保证多节点操作的原子性。

  • 事务的一致性

    当发生网络传输故障或者节点故障,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。

  • 事务的隔离性

    事务隔离性的本质就是如何正确处理多个并发事务的读写冲突和写写冲突,因为在分布式事务控制中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。

    此时并发应用访问数据如果没有加以控制,有可能出现“脏读”问题。

3.2 两阶段提交

3.2.1 简介

  • 概念

    二阶段提交2PC(Two phase Commit)是一种在分布式环境下,所有节点进行事务提交,保持一致性的算法。

  • 角色

    事务的发起者称协调者,事务的执行者称参与者。

    在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。

    2PC通过引入一个协调者(Coordinator)来统一掌控所有参与者(Participant)的操作结果,并指示它们是否要把操作结果进行真正的提交(commit)或者回滚(rollback)。

  • 两阶段

    2PC将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。

3.2.2 提交流程

两阶段提交

  1. 投票(准备)阶段(voting phase)
    1. 参与者通知各协调者(Prepare),等待各协调者反馈结果。
    2. 参与者收到协调者发来的消息后,执行事务操作,将 undo 和 redo 信息记入事务日志中,但并不提交该事务,
    3. 最后参与者返回就绪(同意(Ready,事务参与者本地作业执行成功)或取消(Cancel,本地作业执行故障));
      画外音:可以理解为单机事务的trx.exec()。
  2. 提交阶段(commit phase)
    协调者收到参与者的反馈后(Ready/Cancel),协调者再向参与者发出通知,根据反馈情况,当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务(Commit),否则协调者将通知所有的参与者回滚事务(Rollback)。
    画外音:可以理解为单机事务的trx.commit() 或者 trx.rollback()。

3.2.3 各种情况

  • 三种可能
    在第一阶段协调者的询盘之后,各个参与者会回复自己事务的执行情况,这时候存在三种可能:
  1. 所有的参与者回复能够正常执行事务
  2. 一个或多个参与者回复事务执行失败
  3. 协调者等待超时。
  • 正常情况,协调者将向所有的参与者发出提交事务的通知:
  1. 协调者向各个参与者发送commit通知,请求提交事务。
  2. 参与者收到事务提交通知之后,执行事务commit操作,然后释放占有的资源。
  3. 参与者向协调者返回事务commit结果信息
    2PC成功
  • 第二、三种失败的情况
    协调者均认为参与者无法正常成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:
  1. 协调者向各个参与者发送事务rollback通知,请求回滚事务。
  2. 参与者收到事务回滚通知之后,执行rollback操作,然后释放占有的资源。
  3. 参与者向协调者返回事务rollback结果信息。
  4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。
    2PC事务回滚
    故障情况总结:
  • 协调者故障
    备用协调者接管,并查询参与者执行到什么地址
  • 参与者故障
    协调者会等待他重启然后执行
  • 协调者和参与者同时故障
    协调者故障,然后参与者也故障。例如:有机器 1,2,3,4。其中 4 是协调者,1,2,3是参与者 4 给1,2 发完提交事务后故障了,正好3这个时候也故障了,注意这是 3 是没有提交事务数据的。在备用协调者启动了,去询问参与者,由于3死掉了,一直不知道它处于什么状态(接受了提交事务,还是反馈了能执行还是不能执行 3 个状态)。面对这种情况,2PC,是不能解决的,要解决需要下文介绍的 3PC。

3.2.4 例子

  • 场景
    甲乙丙丁四人要组织一个会议,需要确定会议时间,不妨设甲是协调者,乙丙丁是参与者。
  1. 投票阶段

    (1)甲发邮件给乙丙丁,通知明天十点开会,询问是否有时间;
    (2)乙回复有时间;
    (3)丙回复有时间;
    (4)丁迟迟不回复,此时对于这个事务,甲乙丙均处于阻塞状态,算法无法继续进行;

  2. 提交阶段

    (1)协调者甲将收集到的结果通知给乙丙丁;
    画外音:什么时候通知,以及反馈结果如何,在此例中取决与丁的时间与决定,
    假设丁回复有时间,则通知commit;
    假设丁回复没有时间,则通知rollback;
    (2)乙收到通知,并ack协调者;
    (3)丙收到通知,并ack协调者;
    (4)丁收到通知,并ack协调者;
    画外音:如果甲没有收到所有ack,则分布式事务迟迟不会结束,下一轮投票则迟迟不会开展。

3.2.5 两阶段提交的缺陷

2PC 方案实现起来简单,实际项目中使用比较少,主要因为以下问题:

  1. 性能低-同步阻塞

    2PC在执行过程中,所有节点都处于事务阻塞状态,所有节点所持有的资源(例如数据库数据,本地文件等)都处于锁定状态。

    典型情况为:

    1. 某一个参与者回复消息之前,所有参与者以及协调者都处于阻塞状态;
    2. 在协调者发出消息之前,所有参与者都处于阻塞状态;
  2. 可靠性差-协调者单点问题

    另外,如有协调者或者某个参与者出现了崩溃,为了避免整个算法处于一个完全阻塞状态,即所有的参与者出于锁定事务资源的状态中,无法完成相关的事务操作。此时往往需要借助超时机制来将算法继续向前推进。

  3. 数据不一致-Commit不一定成功

    在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。

    而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

    这个时候需要重试(短时间内可能无法充实成功,可以使用一个支持重试的MQ),并且下游需要幂等。

  4. 参与者和协调者同时 down 掉

    协调者在发送完 commit 消息后 down 掉,而唯一接受到此消息的参与者也 down 掉了。新协调者接管,也是一个懵逼的状态,不知道此条事务的状态。无论提交或者回滚都是不合适的。这个是两阶段提交无法改变的

总的来说,2PC是一种比较保守并且低效的算法,分布式事务真的很难做。

3.2.6 XA

XA是由X/Open组织提出的分布式事务的规范,主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。

XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。

  • 事务管理器
    XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer等的论文),**两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。**事务管理器控制着全局事务,管理事务生命周期,并协调资源。
  • 资源管理器
    资源管理器负责控制和管理实际资源(如数据库或JMS队列)。数据库就是一种资源管理器。资源管理还应该具有管理事务提交或回滚的能力。

下图说明了事务管理器™、资源管理器(RM),与应用程序(AP)之间的关系:

XA
由全局事务管理器管理和协调的事务,可以跨越多个资源(如数据库或JMS队列)和进程。 全局事务管理器一般使用 XA 二阶段提交协议与数据库进行交互。

  • 代码示例

  • 应用与XA

    MySQL从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。

3.2.7 JTA

作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。

像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:

  1. J2EE容器所提供的JTA实现(JBoss)
  2. 独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。

3.3 三阶段提交

3.3.1 简介

由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。

针对两阶段提交存在的问题,三阶段有两个改进点:

  1. 通过引入一个preCommit“预询问”阶段,使得原先在第二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
  2. 在协调者和参与者中都加入超时策略来减少整个集群的阻塞时间,提升系统性能。

三阶段提交的分别为:can_commitpre_commitdo_commit

三阶段
与两阶段提交不同的是,三阶段提交有两个改动点:

  1. 引入超时机制。同时在协调者和参与者中都引入超时机制,防止因为某个角色故障导致整个链路全局阻塞。
  2. 在第一阶段和第二阶段之间插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的投票阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

3.3.2 CanCommit

3PC的CanCommit阶段其实和2PC的投票阶段很像。参与者根据自身情况回复一个预估值(YES/NO),相对于真正的执行事务,这个过程是轻量的,具体步骤如下:

  1. 事务询问
    协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈
    参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态;否则反馈No。

这个和 2PC 阶段不同的是,此时参与者没有锁定资源,没有写 redo,undo,执行回滚日志。回滚代价低

3.3.3 PreCommit

本阶段协调者会根据第一阶段CanCommit的询问结果采取相应操作:

  1. 所有的参与者都返回确定信息
  2. 一个或多个参与者返回否定信息
  3. 协调者等待超时

针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:

  1. 发送PreCommit
    协调者向所有的事务参与者发送事务执行通知,进入Prepared阶段。
  2. 事务PreCommit
    参与者收到PreCommit请求后,执行事务,并将undo和redo信息记录到事务日志中,但不提交该事务
  3. 响应反馈
    参与者将事务执行情况返回给客户端(ACK响应)时开始等待协调者的最终事务指令。
    PreCommit1
    在上面的步骤中,如果参与者等待超时,则会中断事务。

针对第二、三种异常情况,协调者认为事务无法正常执行,于是向各个参与者发出abort通知,请求退出Prepared状态,具体步骤如下:

  1. 发送事务中断请求
    协调者向所有事务参与者发送abort通知
  2. 事务中断
    参与者收到abort通知后(或超时之后,仍未收到协调者的请求),中断当前事务
    CanCommit失败

3.3.4 DoCommit

如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:

  1. 所有的参与者都能正常执行事务
  2. 一个或多个参与者执行事务失败
  3. 协调者等待参与者反馈超时

针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:

  1. 协调者向所有参与者发送事务commit通知
  2. 所有参与者在收到doCommit之后提交事务,并释放占有的资源
  3. 参与者向协调者反馈事务提交结果
  4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
    DoCommit成功
    针对第二、三种异常情况,协调者认为事务无法正常执行,于是向各个参与者发送事务回滚请求,具体步骤如下:
  5. 协调者向所有参与者发送事务rollback通知
  6. 所有参与者在收到通知之后使用undo信息执行rollback操作,并释放占有的资源
  7. 参与者向协调者反馈事务提交结果
  8. 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断
    DoCommit失败
    注意:在DoCommit阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,此时参与者将不会如两阶段提交中那样陷入阻塞,而是在等待超时后直接commit事务。这样处理的原因是,能够进入此阶段,说明在事务询问阶段所有节点都是好的。即使在提交的时候部分失败,有理由相信,此时大部分节点都是好的。是可以提交的

3.3.5 2PC与3PC的区别

  • 优点

    • 3PC解决了减少阻塞和协调者单点问题
      相对于2PC,3PC主要减少阻塞和解决协调者单点,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,而不会一直持有事务资源并处于阻塞状态。

    这样,就算是在阶段3协调者挂了,参与者继续提交事务。

  • 缺点

    • 没解决数据一致性问题

      但是这种机制也会导致数据一致性问题,因为在DoCommit阶段,由于网络原因,协调者发送的rollback响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到rollback命令并执行回滚的参与者之间存在数据不一致的情况。

      在分布式数据库中,如果期望达到数据的强一致性,那么服务基本没有可用性可言,这也是为什么许多分布式数据库提供了跨库事务,但也只是个摆设的原因,在实际应用中我们更多追求的是数据的弱一致性或最终一致性,为了强一致性而丢弃可用性是不可取的。

3.4 阿里Seata(原Fescar)

解决同步事务。

seata 之类分布式事务的性能评估不是 吞吐量 而是损耗,即因为引入分布式事务, 你的RT增加了多少比例。

3.4.1 微服务与分布式事务

微服务倡导将复杂的单体应用拆分为若干个功能简单、松耦合的服务,这样可以降低开发难度、增强扩展性、便于敏捷开发。当前被越来越多的开发者推崇。

系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。每一个微服务内部的数据一致性仍有本地事务来保证。而整个业务层面的全局数据一致性需要一个分布式事务的解决方案保障业务全局的数据一致性。

微服务与分布式事务

3.4.2 Fescar简介

由2014年阿里中间件团队发布 TXC(Taobao Transaction Constructor)改造来的阿里分布式事务解决方案:GTS(Global Transaction Service),已正式推出开源版本,取名为“Fescar”,希望帮助业界解决微服务架构下的分布式事务问题。

现在更名为Seata。

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

参考文档:

  • AT TCC Saga模式图文详解

3.4.3 设计思想

3.4.3.1 设计初衷

高速增长的互联网时代,快速试错的能力对业务来说是至关重要的:

  • 接入成本低,对业务无侵入

    不应该因为技术架构上的微服务化和分布式事务支持的引入,给业务层面带来额外的研发负担。

    这里的“侵入”是指,因为分布式事务这个技术问题的制约,要求应用在业务层面进行设计和改造。这种设计和改造往往会给应用带来很高的研发和维护成本。把分布式事务问题在中间件这个层次解决掉,不要求应用在业务层面做额外工作。

  • 高性能

    引入分布式事务支持的业务应该基本保持在同一量级上的性能表现,不能因为事务机制显著拖慢业务。

    具体来说,引入分布式事务的保障,必然会有额外的开销,引起性能的下降。我们希望把分布式事务引入的性能损耗降到非常低的水平,让应用不因为分布式事务的引入导致业务的可用性受影响。

3.4.3.2 既有方案问题

既有的分布式事务解决方案按照对业务侵入性分为两类,即:对业务无侵入的和对业务有侵入的。

  • 业务无侵入的方案-XA

    既有的主流分布式事务解决方案中,对业务无侵入的只有基于 XA 的方案,但应用 XA 方案存在 3 个方面的问题:

    • 要求数据库提供对 XA 的支持

      如果遇到不支持 XA(或支持得不好,比如 MySQL 5.7 以前的版本)的数据库,则不能使用。

    • 受协议本身的约束,事务资源的锁定周期长

      长周期的资源锁定从业务层面来看,往往是不必要的,而因为事务资源的管理器是数据库本身,应用层无法插手。这样形成的局面就是,基于 XA 的应用往往性能会比较差,而且很难优化。

    • 已经落地的基于 XA 的分布式解决方案,都依托于重量级的应用服务器(Tuxedo/WebLogic/WebSphere 等),这是不适用于微服务架构的。

  • 侵入业务的方案

    实际上,最初分布式事务只有 XA 这个唯一方案。XA 是完备的,但在实践过程中,由于种种原因(包含但不限于上面提到的 3 点)往往不得不放弃,转而从业务层面着手来解决分布式事务问题。比如:

    • 基于可靠消息的最终一致性方案
    • TCC
    • Saga

    都属于这一类。这些方案的具体机制在这里不做展开,网上这方面的论述文章非常多。总之,这些方案都要求在应用的业务层面把分布式事务技术约束考虑到设计中,通常每一个服务都需要设计实现正向和反向的幂等接口。这样的设计约束,往往会导致很高的研发和维护成本。

3.4.3.3 理想的方案应该是什么样子?

不可否认,侵入业务的分布式事务方案都经过大量实践验证,能有效解决问题,在各种行业的业务应用系统中起着重要作用。但回到原点来思考,这些方案的采用实际上都是迫于无奈。设想,如果基于 XA 的方案能够不那么重,并且能保证业务的性能需求,相信不会有人愿意把分布式事务问题拿到业务层面来解决。

一个理想的分布式事务解决方案应该:像使用本地事务一样简单,业务逻辑只关注业务层面的需求,不需要考虑事务机制上的约束。

3.5 本地事务消息表(异步确保)

详细实现思路可参考

解决异步事务。

3.5.1 概述

该实现方式应该是业界使用最多,其核心思想是将分布式事务拆分成本地事务进行处理,来源于ebay。

我们可以从下面的流程图中看出其中的一些细节:

本地消息表
基本思路就是:

  • 上游事务-消息生产方

    消息生产方(也就是发起方)需要在数据库额外建一个本地消息表,并记录消息发送状态。写消息表和写业务数据的操作要在一个事务里提交,也就是说他们要在一个数据库里面。

    消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送,直到发送成功即MQ响应了ACK。

  • 下游事务-消息消费方

    查询自己的那份本地消息表,如果已经消费过的则不处理;否则需要处理这个消息,并完成自己的业务逻辑,最后将业务数据和消息处理成功写入本地消息表两个操作放到一个事务里,以避免重复消费问题。

    此时如果本地事务处理成功,表明已经处理成功了,就更新本地消息表记录该条消息已经处理;如果处理失败,那么就会重试执行。

    如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

  • 处理未完成消息

    生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

  • 小结

    该方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。

    • 优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
    • 缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。而且,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。

3.5.2 电商下单与扣库存例子

库存服务和订单服务分别在不同的服务器节点上,下面把分布式事务最先开始处理的事务方称为事务主动方(库存服务),在事务主动方之后处理的业务内的其他事务称为事务被动方(订单服务)。

事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。

整个业务处理流程如下:

本地消息表

  1. 事务主动方处理本地事务。

    事务主动方在本地同一个事务中处理业务更新操作和写消息表操作。

    上面例子中库存服务阶段在本地同个事务中完成扣减库存和写消息表(图中 1、2)。

  2. 事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。

    消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。

    上面例子中,库存服务把事务待处理消息写到消息中间件,下游的订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。

  3. 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。

    上面例子中,订单服务把事务已处理消息写回到消息中间件,然后库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。

  • 错误处理与幂等
    **为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。**具体保存一致性的容错处理如下:
    • 当步骤 1 处理上游业务逻辑出错,事务回滚,相当于什么都没发生。

    • 当步骤 2、步骤 3 下游事务处理出错,由于未处理的事务消息还是保存在事务发送方本地消息表中,事务发送方可以定时轮询为超时消息数据,再次发送到消息中间件通知事务被动方进行处理。

      事务被动方消费事务消息重试处理。

    • 如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。

    • 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知所有事务被动方回滚。

3.5.3 购买与kafka例子

  • producer:
@Transactionpublic void buy(){
user.buy();//新建一张msgtable本地消息表,消息在这里插入insertInitMsgToDB();}public void sendMsg(){
//发送消息移到了新方法里kafkaTemplete.sendMdg();}//主流程执行buy();sendMsg();
  • consumer:
@Kafkalistener@Transactionpublic void msgConsume(Record record){
if(isConsumed(record)){
//查询本地消息表,已经消费过的则不处理 return; } //处理业务逻辑 deal(record); // 更改本地消息表消息状态为成功 changeRecord(record); // 还可以加逻辑,如果业务逻辑处理失败就给生产者发送消息,让他进行补偿事务等处理}
  • MQ带来的消息重复或丢失的问题
    kafka中的配置enable.auto.commit 是 true,就有可能导致at least once,或者at most once的问题:
    • at most once
      当到达提交时间间隔,触发Kafka自动提交上次的偏移量时,就可能发生at most once的情况,在这段时间,如果消费者还没完成消息的处理进程就崩溃了, 消费者进程重新启动时,它开始接收上次提交的偏移量之后的消息,实际上消费者可能会丢失几条消息;
    • at least once
      而当消费者处理完消息并将消息提交到持久化存储系统,而消费者进程崩溃时,会发生at least once的情况。 在此期间,kafka没有向broker提交offset,因为自动提交时间间隔没有过去。 当消费者进程重新启动时,会收到从上次提交的偏移量开始的一些旧消息。
    • exactly once
      1. enable.auto.commit:false,消费数据、业务流程完成后再手动提交offset
      2. request.required.acks:-1,生产者必须等ISR全部确认后才返回
      3. 业务逻辑代码去重,比如增加消费表每次先去查下没有才插入或用redis。

3.5.4 在线支付系统的跨行转账例子

  1. 用户A事务
Begin transaction         // 对用户id为A的账户扣款1000元update user_account set amount = amount - 1000 where userId = 'A' // 通过本地事务将事务消息(包括本地事务id、支付账户、收款账户、金额、状态等)插入至消息表:  insert into trans_message(xid,payAccount,recAccount,amount,status) values(uuid(),'A','B',1000,1);、end transactioncommit;
  1. 用户B事务
    通知对方用户id为B,增加1000元。通常通过MQ的方式发送异步消息,对方订阅并监听消息后自动触发转账的操作;

这里为了保证幂等性,防止触发重复的转账操作,需要在执行转账操作方新增一个trans_recv_log表用来做幂等:在第二阶段用户B收到消息后,通过判断trans_recv_log表来检测相关记录是否被执行,如果未被执行则会对B账户余额执行加1000元的操作,并会将该记录增加至trans_recv_log,事件结束后通过回调更新trans_message的状态值。

Begin transaction  /**读取消息, B账户加1000.....*/update trans_message set status = 0 where xid = ?end transactioncommit;

3.5.5 本地消息表总结

  • 方案的优点如下:

    • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
    • 方案轻量,容易实现。
  • 缺点如下:

    • 与具体的业务场景绑定,耦合性强,不可公用。
    • 消息数据与业务数据同库,占用业务系统资源。
    • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
  • 重点

    • 事务主动方基于 MQ 通信通知事务被动方处理事务,事务被动方基于 MQ 返回处理结果。
    • 如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。
    • 如果是事务被动方业务上的处理失败,可以通过 MQ 通知事务主动方进行补偿或者事务回滚。

3.5.6 异步确保(事务消息)

  • 事务-理想化方案

    事务消息实际上是一个很理想的想法:我们只要把消息扔到MQ,那么这个消息肯定会被消费成功。生产方不用担心消息发送失败,也不用担心消息会丢失。事务消息,关键一点是把上小节中繁琐的消息状态和重发等用中间件形式封装了。

    回到现实,消费方如果消息处理失败了,还有机会继续消费,直到成功为止(消费方逻辑bug导致消费失败情况不在本文讨论范围内)。

    但遗憾的是市面上大部分MQ都不支持事务消息,其中包括看起来可以一统江湖的kafka。

    RocketMQ号称支持,但是还没开源(事务消息相关部分没开源)。阿里云据说免费提供,没玩过(羡慕下阿里等大厂内部猿类们)。不过从网上公开的资料看,用起来还是有些不爽的地方。这是后话了,毕竟解决了很多问题。

    异步确保(事务消息)
    下面以网传RMQ为例,说明事务消息大概是怎么玩的:
    RMQ的事务消息相对于普通MQ,相当于提供了2PC的提交接口:

  1. Prepared B:生产方需要先发送一个prepared消息给RMQ。如果操作1失败,返回失败。

  2. 执行本地事务A,

  3. Confirm B:步骤2如果成功则需要发送Confirm B消息给RMQ,然后B处于Ready状态;步骤2失败,则调用RMQ cancel接口。

  4. CheckTransaction:步骤3发送Confirm消息失败了(或者超时)该如何处理呢?

    RMQ会要求生产方实现一个check的接口,并告知RMQ自己本地事务是否执行成功(第4步)。RMQ会定时轮询所有处于pre状态的消息,并调用对应的check接口,以决定此消息是否可以提交。

  5. Consume:事务被动方B消费到B事务消息,并执行本地事务B。当然第5步也可能会失败。这时候需要RMQ支持消息重试。处理失败的消息过段时间再进行重试,直到成功为止(超过重试次数后会进死信队列,可能得人肉处理了,因为没用过所以细节不是很了解)。

RMQ还是很强大的。我们认为这个程度的一致性已经能够满足绝大部分互联网应用场景。代价是生产方做了不少额外的事情,但相比没有事务消息情况,确实解放了不少劳动力。

3.6 MQ-最终一致性

3.6.1 简介

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相对于提供了 2PC 的提交接口,核心思想:

先往MQ推half半事务,但此时不会投递给订阅者。然后处理该事务,最后根据结果发送commit或rollback给MQ,如果是commit就发送half给订阅方执行下游事务;否则抛弃该条消息。

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

使用消息中间件MQ,可能由于消费者挂掉或网络波动,短时间无法消费消息,但生产者只关心消息是否发出去,而不关心是否被消费,所以这就是上面CAP理论中的最终一致性,也是弱一致性。

RabbitMQ
优点: 实现了最终一致性,不需要依赖本地数据库事务。

3.6.2 正常情况:事务主动方发消息

MQ2

事务主动方服务正常,没有发生故障,发消息流程如下:

  1. 发送方向 MQ Server发送 half即半事务 消息。
  2. MQ Server 将消息持久化成功之后,向发送方返回ack,确认消息已经发送成功。
  3. 发送方开始执行本地事务逻辑。
  4. 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
  5. 如果MQ Server 收到 commit 状态后,将half消息标记为可投递,订阅方最终将收到该消息;若MQ Server 收到 rollback 状态则删除half消息,订阅方将不会接受该消息。

3.6.3 异常情况:事务主动方消息恢复

异常情况:事务主动方消息恢复在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

图5:MQ Server 对该消息发起消息回查。
图6:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
图7:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
图8:MQ Server基于 commit/rollback 对消息进行投递或者删除。

3.6.4 小结

  • 相比本地消息表方案,MQ 事务方案优点是:
    • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
    • 吞吐量高于使用本地消息表方案。
  • 缺点是
    • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
    • 业务处理服务需要实现消息状态回查接口,用以在异常情况时MQ来调用进行回查。
    • 需要MQ支持half语义事务,比如这里用到的RocketMQ

3.7 支付宝回调

做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。

其实这就是一个很典型的补偿例子,跟一些MQ重试补偿机制很类似。

一般成熟的系统中,对于级别较高的服务和接口,整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题,那么这种重试机制其实是非常有效的。

当然,考虑个比较极端的场景,假如系统自身有bug或者程序逻辑有问题,那么重试1W次那也是无济于事的。那岂不是就发生了“明明已经付款,却显示未付款不发货”类似的悲剧?

其实为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。

在某些特殊的情况下,还会有“人工补偿”的,这也是最后一道屏障。

3.8 Saga 事务:最终一致性

saga是 分布式事务 最主流的模式

3.8.1 简介

Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

3.8.2 处理流程

  • Saga 事务基本协议如下:
    • 每个 Saga 事务由一系列幂等有序子事务(sub-transaction) Ti 组成。
    • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果
      可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。

3.8.3 用户下单例子

3.8.3.1 场景介绍

下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。

用户下单
Saga 的执行顺序有两种,如上图:

  • 事务正常执行完成

    T1, T2, T3, …, Tn,例如:扣减库存(T1),创建订单(T2),支付(T3)等,依次有序完成整个事务。

  • 事务回滚

    T1, T2, …, Tj, Cj,…, C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。

3.8.3.2 Saga 定义了两种恢复策略
  • 向前恢复(forward recovery)
    对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的子事务(sub-transaction)。该情况下不需要Ci(撤销事务)。如下图:
    向前恢复
  • 向后恢复(backward recovery)
    对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。如下图:
    向后恢复
3.8.3.3 Saga事务实现之命令协调
  • 概念

    命令协调(Order Orchestrator)即中央协调器负责集中处理事件的决策和业务逻辑排序。

    中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。

  • 以电商订单的例子为例:

    电商订单的例子

  1. 事务发起方的主业务逻辑请求 Sage协调器OSO 服务开启订单事务
  2. OSO 向库存服务请求扣减库存,库存服务回复处理结果。
  3. OSO 向订单服务请求创建订单,订单服务回复创建结果。
  4. OSO 向支付服务请求支付,支付服务回复处理结果。
  5. 主业务逻辑接收并处理 OSO 事务处理结果回复。
  • 小结

    中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。

    基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

    该方案的问题是协调器OSO存在单点风险。

3.8.3.4 Saga事务实现之事件编排
  • 概念

    事件编排(Event Choreography0):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。

    在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

    当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

  • 以电商订单为例:

    电商订单的例子为例

  1. 事务发起方的主业务逻辑发布开始订单事件。
  2. 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
  3. 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
  4. 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
  5. 主业务逻辑监听订单已支付事件并处理。
  • 小结
    事件编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。

3.8.4 方案总结

  • 命令协调设计的优点如下:

    • 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
    • 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
    • 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。
  • 命令协调设计缺点如下:

    • 中央协调器容易处理逻辑容易过于复杂,导致难以维护。
    • 存在协调器单点故障风险。
  • 事件/编排设计优点如下:

    • 避免中央协调器单点故障风险。
    • 当涉及的步骤较少服务开发简单,容易实现。
  • 事件/编排设计缺点如下:

    • 服务之间存在循环依赖的风险。
    • 当涉及的步骤较多,服务间关系混乱,难以追踪调测。

值得补充的是,由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证事务隔离性。即当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。

4 柔性事务

4.1 柔性事务的概念

在电商等互联网场景下,传统的事务在数据库性能和处理能力上都暴露出了瓶颈。在分布式领域基于 CAP 理论以及 BASE 理论,有人就提出了柔性事务的概念。

基于 BASE 理论的设计思想,柔性事务下,在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。

并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐。

4.2 实现柔性事务的一些特性

下面介绍的是实现柔性事务的一些常见特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样。

4.2.1 可见性(对外可查询)

在分布式事务执行过程中,如果某一个步骤执行出错,就需要明确的知道其他几个操作的处理情况,这就需要其他的服务都能够提供查询接口,保证可以通过查询来判断操作的处理情况。

为了保证操作的可查询,需要对于每一个服务的每一次调用都有一个全局唯一的标识,可以是业务单据号(如订单号)、也可以是系统分配的操作流水号(如支付记录流水号)。除此之外,操作的时间信息也要有完整的记录。

4.2.2 幂等性

幂等性,其实是一个数学概念。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。

幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。也就是说,同一个方法,使用同样的参数,调用多次产生的业务结果与调用一次产生的业务结果相同。

之所以需要操作幂等性,是因为为了保证数据的最终一致性,很多事务协议都会有很多重试的操作,如果一个方法不保证幂等,那么将无法被重试。

幂等操作的实现方式有多种,如在系统中缓存所有的请求与处理结果、检测到重复操作后,直接返回上一次的处理结果等。

5 分布式协议

参考

6 CAP、BAS、数据一致性模型

参考

7 分布式事务小结

XA 事务机制(2PC,3PC)属于CP模型,强一致性,全局锁,效率很低。

更多文章

0xFD 总结

1 各方案使用场景

各方案使用场景

介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景:

  • 2PC/3PC

    依赖于数据库,能够很好的提供强一致性(好像不能保证吧,转载者注)和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。

  • TCC

    适用于执行时间确定且较短,实时性要求高(因为是先提交事务,出错就用补偿事务,而不是2PC/3PC那样的rollback,所以速度更快),对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。

  • 本地消息表/MQ 事务

    都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底

  • Saga 事务

    由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。

    Saga 相比3PC来说缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。

2 分布式事务方案设计

本文介绍的偏向于原理,业界已经有不少开源的或者收费的解决方案,篇幅所限,就不再展开介绍。

实际运用理论时进行架构设计时,许多人容易犯“手里有了锤子,看什么都觉得像钉子”的错误,设计方案时考虑的问题场景过多,各种重试,各种补偿机制引入系统,导致系统过于复杂,落地遥遥无期。

世界上解决一个计算机问题最简单的方法:“恰好”不需要解决它!

—— 阿里中间件技术专家沈询

有些问题,看起来很重要,但实际上我们可以通过合理的设计或者将问题分解来规避。

设计分布式事务系统也不是需要考虑所有异常情况,不必过度设计各种回滚,补偿机制。

如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。

如果系统要实现回滚流程的话,有可能系统复杂度将大大提升,且很容易出现 Bug,估计出现 Bug 的概率会比需要事务回滚的概率大很多。

在设计系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,可以考虑当出现这个概率很小的问题,能否采用人工解决的方式,这也是大家在解决疑难问题时需要多多思考的地方。

3 实施

在理解了各个技术方案的流程,优缺点,使用场景后,结合项目中遇到的具体业务需求和各种资源(人力、设备、时间),再借鉴业界案例,网上资源,试着设计+评审一套基本方案。如资源有限,去github找成熟开源框架或付费方案。开源框架使用前研究下原理,相关配置参数并进行稳定性测试,研究下源码

0xFF 参考文档

你可能感兴趣的文章
程序员--学习之路--技巧
查看>>
解决问题之 MySQL慢查询日志设置
查看>>
contOS6 部署 lnmp、FTP、composer、ThinkPHP5、docker详细步骤
查看>>
TP5.1模板布局中遇到的坑,配置完不生效解决办法
查看>>
PHPstudy中遇到的坑No input file specified,以及传到linux环境下遇到的坑,模板文件不存在
查看>>
TP5.1事务操作和TP5事务回滚操作多表
查看>>
composer install或composer update 或 composer require phpoffice/phpexcel 失败解决办法
查看>>
TP5.1项目从windows的Apache服务迁移到linux的Nginx服务需要注意几点。
查看>>
win10安装软件 打开时报错 找不到 msvcp120.dll
查看>>
PHPunit+Xdebug代码覆盖率以及遇到的问题汇总
查看>>
PHPUnit安装及使用
查看>>
PHP项目用xhprof性能分析(安装及应用实例)
查看>>
composer安装YII
查看>>
Sublime text3快捷键演示
查看>>
sublime text3 快捷键修改
查看>>
关于PHP几点建议
查看>>
硬盘的接口、协议
查看>>
VLAN与子网划分区别
查看>>
Cisco Packet Tracer教程
查看>>
02. 交换机的基本配置和管理
查看>>