分布式事务专题

原创
2023/02/14 11:05
阅读数 238

一、分布式事务之一--什么是分布式事务

1. 前言

什么是分布式事务?介绍这个之前,先来了解一下这几个问题

  1. 什么是事务?
  2. 什么是本地事务?
  3. 什么是分布式?
  4. 什么是分布式事务?

2. 什么是事务?

完成某件事情,可能有多个参与者需要执行多个步骤,最终多个步骤要么全部成功,要么全部失败。

举个例子:微信上A给B转账100元,A账户减少100,B账户增加100,这就是一个事务,这个操作中要么都成功,要么都失败。

事务的场景有很多,参与者也是多种多样

  1. 用户注册成功发送邮件,包含2个操作:db中插入用户信息,给用户发送邮件,主要的2个参与者:db、邮件服务器
  2. 使用支付宝充值话费,包含2个操作:支付宝账户资金减少,手机余额增加,主要的2个参与者:支付宝账户、手机号服务商账户

事务的参与者是多种多样的,不过本文我们主要以db中的事务来做说明。

3. 什么是本地事务?

本地事务,通俗点理解:即事务中所有操作发生在同一个数据库中的情况。

比如A给B转账,A和B的账户位于同一个数据库中。

通常我们用的都是关系型数据库,比如:MySQL、Oracle、SQL Server,这些数据库默认情况,这些db已经实现了事务的功能,即在一个db中执行一个事务操作,db本身就可以确保这个事务的正确性,而不需要我们自己去考虑如何确保事务的正确性。

4. 数据库事务的4大特性

4.1. 一致性(Consistency)

事务操作之后的结果和期望的结果是一致的,A给B转账100,事务结束之后,看到A的账户应该减少100,B的账户应该增加100,不会出现其他情况

4.2. 原子性(Atomicity)

事务的整个过程如原子操作一样,最终要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。

4.3. 隔离性(Isolation)

一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

4.4. 持久性(Durability)

一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘,修改是永久性的。

5. 什么是分布式?

完成某件事情有多个参与者,多个参与者分布在不同的机器中,这些机器之间通过网络或者其他方式进行通讯。

比如使用工行卡给支付宝充值,工行卡的账户位于工商银行的db中,而支付宝账户位于支付宝的db中,2个db位于不同的地方。

6. 什么是分布式事务?

分布式、事务这2个概念大家都理解了,那么分布式事务很容易理解了:事务的多个参与者分布在不同的地方。

单个db中我们很容易确保事务的正确性,但是当事务的参与者位于多个db中的时候,如何确保事务的正确性呢?

比如:A给B转账,A位于DB1中,B位于DB2中

  1. step1.通过网络,给DB1发送指令:给A账户减少100
  2. step2.通过网络,给DB2发送指令:给B账户增加100

step1成功之后,执行step2的时,网络出现故障,导致step2执行失败,最终:A减少了100,B却没有增加100,最终的结果和期望的结果不一致,导致了事务的失败。

在介绍分布式事务的解决方案之前,我们需要先了解另外2个概念:CAP和Base理论,这2个理论为分布式事务的解决提供了依据。

二、分布式事务之二--CAP原则

1. 理解CAP概念

CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。

下边我们分别来解释:

为了方便对CAP理论的理解,我们结合电商系统中的一些业务场景来理解CAP。

如下图,是商品信息管理的执行流程:

整体执行流程如下:

1、商品服务请求主数据库写入商品信息(添加商品、修改商品、删除商品)

2、主数据库向商品服务响应写入成功。

3、商品服务请求从数据库读取商品信息。

1.1. C - Consistency

一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。

上图中,商品信息的读写要满足一致性就是要实现如下目标:

1、商品服务写入主数据库成功,则向从数据库查询新数据也成功。

2、商品服务写入主数据库失败,则向从数据库查询新数据也失败。

如何实现一致性?

1、写入主数据库后要将数据同步到从数据库。

2、写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入从库的过程中,客户端向从数据库查询到旧的数据。

分布式系统一致性的特点:

1、由于存在数据同步的过程,写操作的响应会有一定的延迟。

2、为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。

3、如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。

1.2. A - Availability

可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。

上图中,商品信息读取满足可用性就是要实现如下目标:

1、从数据库接收到数据查询的请求则立即能够响应数据查询结果。

2、从数据库不允许出现响应超时或响应错误。

如何实现可用性?

1、写入主数据库后要将数据同步到从数据库。

2、由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。

3、即使数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。

分布式系统可用性的特点:

1、 所有请求都有响应,且不会出现响应超时或响应错误。

1.3. P - Partition tolerance

通常分布式系统的各个结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,这叫分区容忍性。

上图中,商品信息读写满足分区容忍性就是要实现如下目标:

1、主数据库向从数据库同步数据失败不影响读写操作。

2、其一个结点挂掉不影响另一个结点对外提供服务。

如何实现分区容忍性?

1、尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。

2、添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。

分布式分区容忍性的特点:

1、分区容忍性分是布式系统具备的基本能力

2. CAP组合方式

1、上边商品管理的例子是否同时具备 CAP呢?

在所有分布式事务场景中不会同时具备CAP三个特性,因为在具备了P的前提下C和A是不能共存的。

比如:

下图满足了P即表示实现分区容忍:

本图分区容忍的含义是:

1)主数据库通过网络向从数据同步数据,可以认为主从数据库部署在不同的分区,通过网络进行交互。

2)当主数据库和从数据库之间的网络出现问题不影响主数据库和从数据库对外提供服务。

3)其一个结点挂掉不影响另一个结点对外提供服务。

如果要实现C则必须保证数据一致性,在数据同步的时候为防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败从数据库要返回错误信息或超时信息。

如果要实现A则必须保证数据可用性,不管任何时候都可以向从数据查询数据,则不会响应超时或返回错误信息。

通过分析发现在满足P的前提下C和A存在矛盾性,如下:

网络分区的情况下,主库的数据无法同步给从库,为了确保外面看到数据是一致的,此时从库不能让外部访问,只能让主库对外提供服务,从库失去了可用性。

网络分区的情况下,主库的数据无法同步给从库,此时2个库数据是不一致的,如果此允许2个库都可以对外提供服务(可用性),那么访问到的数据是不一致的。

所以CAP无法同时满足。

2、CAP有哪些组合方式呢?

所以在生产中对分布式事务处理时要根据需求来确定满足CAP的哪两个方面。

1)AP:

放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。

例如:

上边的商品管理,完全可以实现AP,前提是只要用户可以接受所查询的到数据在一定时间内不是最新的即可。

通常实现AP都会保证最终一致性,后面讲的BASE理论就是根据AP来扩展的,一些业务场景 比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。

2)CP:

放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。

3)CA:

放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。

那么系统将不是一个标准的分布式系统,我们最常用的关系型数据就满足了CA。

上边的商品管理,如果要实现CA则架构如下:

主数据库和从数据库中间不再进行数据同步,数据库可以响应每次的查询请求,通过事务隔离级别实现每个查询请求都可以返回最新的数据。

3. CAP小结

通过上面我们已经学习了CAP理论的相关知识,CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99..%),并要达到良好的响应性能来提高用户体验,因此一般都会做出如下选择:保证P和A,舍弃C强一致,保证最终一致性。

三、分布式事务之三--BASE理论

1. 理解强一致性和最终一致性

CAP理论告诉我们一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容忍性(Partition tolerance)这三项中的两项,其中AP在实际应用中较多,AP即舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边我们举的例子主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须一致,它强调的是强一致性,但是最终一致性是允许可以在一段时间内每个结点的数据不一致,但是经过一段时间每个结点的数据必须一致,它强调的是最终数据的一致性。

2. Base理论介绍

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

2.1. 基本可用

分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出现问题了,商品依然可以正常浏览。

2.2. 软状态

由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单的”支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。

2.3. 最终一致

最终一致是指经过一段时间后,所有节点数据都将会达到一致。如订单的”支付中”状态,最终会变为“支付成功”或者”支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

四、分布式事务之四--分布式事务常见5种解决方案

分布式事务常见的7种解决方案。

  1. 方案1:2PC(二阶段提交)
  2. 方案2:3PC(三阶段提交)
  3. 方案3:TCC
  4. 方案4:可靠消息
  5. 方案5:最大努力通知型

分布式事务常见5种解决方案--2PC(二阶段提交)

1. 什么是2PC?

2PC即两阶段提交,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

2PC中主要的2个角色:

  1. 事务协调者
  2. 事务参与者

1.1. 准备阶段(prepare)

事务协调者给每个事务参与者发送prepare消息,每个参在本地执行本地事务但是不要提交事务(此时事务操作的资源可能被锁定),然后给协调者返回yes或者no的消息。

1.2. 提交阶段(commit)

准备阶段中所有参与者返回yes,此时事务协调者会给每个事务参与者发送commit消息,参与者接收到commit消息之后,会对本地事务执行提交操作。

若准备阶段中有参与者返回no,或者参与者响应超时(比如网络原因,导致事务协调者和事务参与者之间通讯故障),此时事务协调者会给每个事务参与者发送rollback消息,参与者接收到rollback消息之后,会对本地事务执行回滚操作。

1.3. 2PC 中的一些规则

  1. 阶段2 commit的条件:阶段1中所有的参与者返回yes

  2. 阶段2 rollback的条件,2种情况:阶段1中任意参与者返回no时,或者阶段1中任意参与者响应超时

  3. 当参与者prepare可以成功,那么给参与者发送commit也一定可以成功,发送rollback一定可以回滚

  4. 2PC中事务协调者这边有超时机制,即在阶段1中,协调者给参与者发送消息,一直没有回应,导致超时,此时,直接执行第二阶段rollback;而参与者这边并没有超时机制,比如所有参与者阶段1执行完毕了,然后协调者挂了,此时参与者只能一直等了,干等。

1.4. 2PC 存在的问题

  1. 当阶段一都执行完毕之后,参与者本地事务执行了但是还未提交,此时参与者本地事务中的资源处于锁定状态的,若此时协调者挂了,会导致参与者本地事务锁住的资源无法释放,而直接影响到其他业务的执行。

    比如参与者1中去对商品1减库存,商品1的库存记录会被上锁,若此时其他业务也需要修改这条记录,直接会被阻塞,导致无法执行。

  2. 2PC有性能问题:比如事务中有10个参与者,参与者1在阶段1中会锁定本地资源,然后等待其他9个参与者执行完毕阶段一,然后参与者1收到事务协调器发送的commit或者rollback之后,才会释放资源,参与者1需要等待9个参与者,导致锁定资源的时间太长,会影响系统的并发量。

  3. 协调者有单点故障:当阶段1执行完毕之后,协调者挂了,此时参与者懵了,只能一直等待,这个可以通过协调者高可用来解决,后面讲到的3pc中解决了这个问题。

  4. 事务不一致的问题:阶段2中部分参与者收到了commit信息,此时协调者挂了或者网络问题,导致其他协调者无法收到commit请求,这个过程中,多个协调者中数据是不一致的,解决方式:协调者、参与者要高可用,协调者支持2PC重试,2PC中的2个阶段需要支持幂等。

2. XA事务

XA(eXtended Architecture)是指由X/Open 组织提出的分布式交易处理的规范。XA 是一个分布式事务协议,由Tuxedo 提出,所以分布式事务也称为XA 事务。

XA 协议主要定义了事务管理器TM(Transaction Manager,协调者)和资源管理器RM(Resource Manager,参与者)之间的接口。

其中,资源管理器往往由数据库实现,如Oracle、DB2、MySQL,这些商业数据库都实现了XA 接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。

XA 事务是基于两阶段提交(Two-phaseCommit,2PC)协议实现的,可以保证数据的一致性,许多分布式关系型数据管理系统都采用此协议来完成分布式。阶段一为准备阶段,即所有的参与者准备执行事务并锁住需要的资源。当参与者Ready时,向TM 汇报自己已经准备好。阶段二为提交阶段。当TM 确认所有参与者都Ready 后,向所有参与者发送COMMIT 命令。

说的简单点:XA就是2PC在数据中的一种实现。

mysql大家都用过,普通事务过程:

  1. start transaction; //打开事务
  2. 执行事务操作
  3. commit|rollback; // 提交或者回滚事务

上面事务操作中,若当前连接未发送commit或者rollback操作,此时连接断掉或者mysql重启了,上面的事务会被自动回滚。

mysql中xa的语法:

  1. XA {START|BEGIN} xid [JOIN|RESUME] //开启XA事务,如果使用的是XA START而不是XA BEGIN,那么不支持[JOIN|RESUME],xid是一个唯一值,表示事务分支标识符
  2. XA END xid [SUSPEND [FOR MIGRATE]] //结束一个XA事务,不支持[SUSPEND [FOR MIGRATE]]
  3. XA PREPARE xid 准备提交
  4. XA COMMIT xid [ONE PHASE] //提交,如果使用了ONE PHASE,则表示使用一阶段提交。两阶段提交协议中,如果只有一个RM参与,那么可以优化为一阶段提交
  5. XA ROLLBACK xid //回滚
  6. XA RECOVER [CONVERT XID] //列出所有处于PREPARE阶段的XA事务

如:

  1. xa start 'xa-1';
  2. 执行事务操作;
  3. xa prepare 'xa-1'; //阶段1,此时事务操作的资源被锁住,事务未提交
  4. xa commit | rollback;//阶段2

xa事务和普通事务有点区别,上面这个xa事务有个标识xa-1,当xa-1prepare之后,如果此时连接断掉或者mysql重启了,这个事务还是处于prepare阶段,mysql重启或者调用者重新连接mysql之后,可以拿着这个事务标识xa-1继续发送xa commit |rollback来结束这个事务。

大家可以在mysql中创建几个db,然后通过上面的xa脚本试试两阶段提交,感受一下过程。

3. XA中事务协调器设计要点

XA中,事务参与者,比如常见的一些db,已经实现了2PC的功能,但是协调器需要自己来开发,协调器的一些设计要点:

  1. 生成全局唯一XA事务id记录,并且记录下来
  2. 事务协调器需要有重试的功能,对于中间阶段操作异常的,通过不断的重试让事务最终能够完成
  3. 协调器会有重试操作,所以需确保2pc中每个阶段都是幂等的

4. 2PC解决方案

  1. Seata:Seata是由阿里中间件团队发起的开源项目 Fescar,后更名为Seata,它是一个是开源的分布式事务框架,这个框架支持2PC。
  2. atomikos+jta:jta是java中对分布式事务制定的接口规范,atomikos是jta的一种实现,内部是依靠XA的方式来实现的,如果事务参与者都自测XA事务,可以通过这种方式来解决,比如参与者是:mysql、oracle、sqlserver,可以使用采用这种方式;不过性能方面是值得大家考虑的一个问题。
  3. 开发者自己实现 :大家对2pc过程了解之后,可以自己开发一个,可以去挑战一下。

分布式事务常见5种解决方案--3PC(三阶段提交)

1. 回顾2PC

举个例子,A邀请B、C一起打王者荣耀,2PC过程如下:

A是协调者,B、C是参与者。

1.1. 阶段1(prepare阶段)

(1)、step1-1:A微信B

  1. step1-1-1A->B:有空么,我们约C一起王者荣耀
  2. step1-1-2B->A:有空
  3. step1-1-3A->B:那你现在就打开电脑,登录王者荣耀,你等着,我去通知C,然后开个房间
  4. step1-1-4B->A:已登录

(2)、step1-2:A微信C

  1. step1-2-1A->C:有空么,我约了B一起王者荣耀
  2. step1-2-2C->A:有空
  3. step1-2-3A->C:那你现在就打开电脑,登录王者荣耀,你等着,我去开个房间
  4. step1-2-4C->A:已登录

1.2. 阶段2(commit阶段)

此时B、C都已经登录王者容易了,然后A登录王者荣耀开了个房间

(1)、step2-1:A微信B

  1. step2-1-1A->B:房间号是xxx,你可以进来了
  2. step2-1-2B->A:我的,我进来了

(2)、step2-2:A微信C

  1. step2-2-1A->C:房间号是xxx,你可以进来了
  2. step2-2-2C->A:我的,我进来了

然后3个人开始爽歪歪了。

1.3. 2PC一些异常情况

(1)、情况1:step1-2-4超时,导致A无法收到C已登录的消息

此时A不知道C是什么情况,但是2PC中协调者这边有超时机制,如果协调者给参与者发送信息,长时间得不到回应时,将作为失败处理,此时A会给B和C发送rollback消息,让B和C都进行回滚,即取消游戏。

(2)、情况2:step1-1之后,协调者A挂了

此时B已经打开电脑在那等着了,却始终不见A、C的踪影,相当苦恼,也不知道还要等多久,苦逼!

(3)、情况3:阶段1之后,协调者A挂了

此时B、C登录账号了,也等了十几分钟了,就是不见A的踪影,也只能干等着,什么事情也做不了。

(4)、情况4:step2-2-1出现问题,C网络故障

此时C收不到A发送过来的消息,结果是导致A和B都已经进入房间了,就缺C了,游戏无法正常开始,导致最终的结果和期望的结果无法一致(期望3个人一起玩游戏,实际上房间里只有2个人)

1.4. 总的来说,2PC主要有2个问题

(1)、参与者干等的问题

参与者只能按照协调者的指令办事,当收不到协调者的指令的时候,参与者只能坐等,在db中的效果,操作的数据会被一直锁着,导致其他操作者被阻塞。

(2)、数据不一致的问题

commit阶段,协调者或者参与者挂掉,都可能导致最终数据不一致的问题。

2. 3PC

3PC主要解决了2PC中commit阶段参与者干等的问题,2PC中commit阶段,若协调者挂了,参与者不知道如何走了。2PC中只有协调者这边有超时机制,而3PC中,协调者和参与者这边引入了超时机制,commit阶段,若参与超过一定的时间收不到commit命令,参与者会自动提交,从而解决了2PC中资源长时间被锁的问题。

3PC相对于2PC,多了一个阶段,相当于把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommitPreCommitDoCommit三个阶段。

2.1. 阶段1:CanCommit阶段

之前2PC的一阶段是本地事务执行结束后,最后不Commit,等其它服务都执行结束并返回Yes,由协调者发出commit才真正执行commit,而这里的CanCommit指的是 尝试获取数据库锁 如果可以,就返回Yes。

这阶段主要分为2步

事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与的响应。

响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No,然后事务就结束了,此时参与者并没有执行任务任何操作。

2.2. 阶段2:PreCommit阶段

在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。这里的PreCommit阶段 跟上面的第一阶段是差不多的,只不过这里 协调者和参与者都引入了超时机制(2PC中只有协调者可以超时,参与者没有超时机制)。

2.3. 阶段3:DoCommit阶段

这里跟2pc的阶段二是差不多的。

3. 案例:王者荣耀3PC过程

3.1. 正常的过程

(1)、阶段1(CanCommit阶段)

step1-1:A微信B

  1. step1-1-1A->B:有空么,我们约C一起王者荣耀
  2. step1-1-2B->A:有空

step1-2:A微信C

  1. step1-1-1A->B:有空么,我们约B一起王者荣耀
  2. step1-1-2B->A:有空

(2)、阶段2(PreCommit阶段)

step2-1:A微信B

  1. step2-1-1A->B:你现在就打开电脑,登录王者荣耀,等我消息,如果10分钟没消息,你就自己开个房间玩吧(参与者超时机制)。
  2. step2-1-2B->A:已登录

step2-2:A微信C

  1. step2-2-1A->C:那你现在就打开电脑,登录王者荣耀,等我消息,如果10分钟没消息,你就自己开个房间玩吧(参与者超时机制)。
  2. step2-2-2C->A:已登录

(3)、阶段3(DoCommit阶段)

此时B、C都已经登录王者容易了,然后A登录王者荣耀开了个房间

step3-1:A微信B

  1. step3-1-1A->B:房间号是xxx,你可以进来了
  2. step3-1-2B->A:我的,我进来了

step3-2:A微信C

  1. step3-2-1A->C:房间号是xxx,你可以进来了
  2. step3-2-2C->A:我的,我进来了

然后3个人开始爽歪歪了。

3.2. 异常的几种情况

(1)、阶段1异常

此时并没有进行事务操作,所以这个阶段出问题了,可以直接结束事务。

(2)、阶段2,参与者挂了

参与者挂了没关系,协调者直接通知其他参与者回滚。

(3)、阶段2,协调者挂了

协调者挂了,由于参与者引入了超时机制,所以参与者并不会无限期等待,等待一定的时间之后,会自动提交本地事务。

虽然这个超时机制解决了无限等待的问题,却并没有解决一致性的问题,比如上面3PC中step2-1:A微信B之后,协调者挂了,此时A已经登录了,但是C未收到A要求登录的消息,超时10分钟之后,A自己去开了一个游戏玩起来了,结果和期望的结果不一致了。

4. 3PC存在的问题

虽然解决了2PC中参与者长时间阻塞的问题(资源长时间无法释放的问题),但是并没有解决一致性的问题。

有没有办法解决这些问题?

有,TCC,接下来,我们来看TCC。

分布式事务常见5种解决方案--TCC(TCC)

1. 什么是TCC?

分布式事务中的几个角色

  • TM:事务管理器,可以理解为分布式事务的发起者
  • 分支事务:事务中的多个参与者,可以理解为一个个独立的事务。

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。

Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。

TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。

1.1. 正常流程

try阶段:依次调用参与者的try方法,都返回成功

confirm阶段:依次调用参与者的confirm方法,都返回成功

事务完成。

1.2. 异常流程

try阶段:依次调用参与者的try方法,前面2个参与者try方法返回yes,而参与者3返回no

cancel阶段:对已经成功的参与者执行cancel操作,注意了:cancel阶段参与者调用的顺序和try阶段参与者的顺序相反,即先调用参与者2的cancel,然后调用参与者1的cancel

2. TCC场景案例

2.1. 案例1:跨库转账

举例,场景为 A 转账 100 元给 B,A和B账户在不同的服务。

 
  1. 账户A
  2. try
  3. try幂等校验
  4. 检查余额是否够100
  5. A账户扣减100
  6. confirm
  7. cancel
  8. cancel幂等校验
  9. A账户增加可用余额100
  10.  
  11. 账户B
  12. try
  13. confirm
  14. confirm幂等校验
  15. B账户增加100
  16. cancel

2.2. 案例2:提现到支付宝

举例,大家玩过抖音,有些朋友抖音上面有收益,可以将收益提现到支付宝,假如提现100到支付宝

 
  1. 抖音(账户表:余额、冻结金额)
  2. try
  3. try幂等校验
  4. 检查余额是否够100
  5. 抖音账户表余额-100,冻结金额+100
  6. confirm
  7. confirm幂等校验
  8. 抖音账户冻结金额-100
  9. cancel
  10. cancel幂等校验
  11. 抖音账户表余额+100,冻结金额-100
  12.  
  13. 账户B
  14. try
  15. confirm
  16. confirm幂等校验
  17. 调用支付宝打款接口,打款100元(对于商户同一笔订单支付宝接口是支持幂等的)
  18. cancel

3. TCC常见框架

框架名称 github地址 star数量
tcc-transaction https://github.com/changmingxie/tcc-transaction 4750
hmily https://github.com/Dromara/hmily 2900
ByteTCC https://github.com/liuyangming/ByteTCC 2450
EasyTransaction https://github.com/QNJR-GROUP/EasyTransaction 2100

4. 自研TCC框架设计思路

4.1. 涉及到的角色(事务发起者、事务参与者、TCC服务)

(1)、事务发起者(TM)

  • 发起分布式事务:调用tcc服务注册一个分布式事务订单

  • 调用分支:依次调用每个分支

  • 上报结果:最终将事务所有分支的执行结果汇报给TCC服务

  • 提供补偿接口:给TCC服务使用,tcc服务会调用这个补偿接口对进行补偿操作

(2)、事务参与者

  • 提供3个方法:try、confirm、cancel
  • 确保3个方法的幂等性
  • 3个方法返回的结果状态码只有3种(成功、失败、处理中),处理中相当于状态未知,对于状态未知的,会在补偿的过程中进行重试

(3)、TCC服务

  • 是一个独立的服务
  • 提供分布式事务订单注册接口:给事务发起者使用【事务发起者调用tcc服务生成一个分布式事务订单(订单状态:0:处理中,1:处理成功,2:处理失败),获取一个分布式订单id:TID】
  • 提供分布式事务结果上报接口:给事务发起者使用【事务发起者在事务的执行过程中将事务的执行结果汇报给TCC服务】
  • 提供事务补偿操作:启动一个job轮询tcc订单中状态为1的订单,继续调用事务发起者进行补偿,最终经过多次补偿,这个订单最终的状态应该为1(成功)或者2(失败);否则人工介入进行处理

4.2. 时序图

4.3. 自研TCC框架技术要点

(1)、框架应该考虑的地方

开发者应该只用关注分支中3个方法的代码,其他的应该全部交由框架去完成。

(2)、tcc服务中的事务订单表设计

  • id:订单id
    • bus_order_id:业务方订单id
    • bus_order_type:业务类型 (bus_order_id & bus_order_type 需唯一)
    • request_data:业务请求数据,json格式存储,包含了玩转的业务方请求数据
    • status:状态,0:处理中,100:处理成功,200:处理失败,初始状态为0,最终必须为100或者200

(3)、关于分支中3个方法幂等的设计

以java中的spring为例,可以通过拦截器来实现,拦截器对分支的3个方法进行拦截,拦截器中实现幂等性的操作。

可以用一张表来实现【分支方法执行记录表:tid、分支、方法(try、confirm、cancel)、状态(0:处理中;100:成功;200:失败)、request_json(请求参数)、response_json(响应参数)】

关于请求参数:这个用来记录整个方法请求的完整参数,内部包含了业务参数,可以采用json格式存储。

响应参数:分支方法的执行结果,以json格式存储。

拦截器中,通过分支 & 方法 这2个条件去查询分支方法执行记录表,如果查询的记录状态为100或者200,那么直接将response_json返回。

(4)、try阶段同步、其他阶段异步

如果try阶段全部成功,那么confirm阶段最终应该一定是成功的,try阶段如果有失败的,那么需要执行cancel,最终所有的cancel应该也是一定可以成功的;所以try阶段完成之后,其实已经知道最终的结果了,所以try阶段完成之后,后面的confirm或者cancel可以采用异步的方式去执行;提升系统整体的性能。

(5)、异步上报事务执行结果

发起方将所有分支每个步骤的执行结果及最终事务的执行结果上报给tcc服务,由tcc服务落库,方便运营人员查看事务执行结果以及排错。

(6)、关于补偿

tcc服务中添加一个补偿job,定时轮询tcc分布式订单表,将状态为处理中的记录撸出来,订单表request_data包含了请求参数,使用request_data去调用事务发起者提供的补偿接口进行补偿操作,直到订单的状态为最终状态(成功或者失败)。

补偿采用衰减的形式,对应同一笔订单采用时间间隔衰减的方式补偿,每次间隔时间:10s、20s、40s、80s、160s、320s。。。

(7)、人工干预

tcc分布式订单如果长期处于处理中,经过了很多次的补偿,也未能到达最终状态,此时可能业务有问题,需要人工进行补偿,对于这对订单记录需要有监控系统进行报警,提醒开发者进行干预处理。

5. 小结

如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,是2PC在应用层面的一种实现,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,代码量比较大。

分布式事务解决方案之可靠消息最终一致性

1. 什么是可靠消息最终一致性?

可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能
够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。

这里面有2个重点:

  1. 消息发送方本地事物执行成功之后,消息一定会投递成功
  2. 消息消费者最终也一定能够消费此消息,最终使分布式事务最终达成一致性

2. 业务场景:下单送积分

电商中有这样的一个场景:商品下单之后,需给用户送积分,订单表和积分表分别在不同的db中,涉及到分布式事务的问题。

我们通过可靠消息来解决这个问题:

  1. 商品下单成功之后送积分的操作,我们使用mq来实现
  2. 商品下单成功之后,投递一条消息到mq,积分系统消费消息,给用户增加积分

我们主要讨论一下,商品下单及投递消息到mq的操作,如何实现?每种方式优缺点?

3. 投递消息过程:方式一

3.1. 过程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:投递消息到mq
  • step4:提交本地事务

这种方式是将发送消息放在了事务提交之前。

3.2. 可能存在的问题

  • step3发生异常:导致step4失败,商品下单失败,直接影响到商品下单业务
  • step4发生异常,其他step成功:商品下单失败,消息投递成功,给用户增加了积分

4. 投递消息过程:方式二

下面我们换种方式,我们将发送消息放到事务之后进行。

4.1. 过程

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:提交本地事务
  • step4:投递消息到mq

4.2. 可能会出现的问题

step4发生异常,其他step成功:导致商品下单成功,投递消息失败,用户未增加积分

上面两种是比较常见的做法,也是最容易出错的。

5. 投递消息过程:方式三

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:本地库中插入一条需要发送消息的记录t_msg_record
  • step3:提交本地事务
  • step5:新增一个定时器,轮询t_msg_record,将待发送的记录投递到mq中

这种方式借助了数据库的事务,业务和消息记录作为了一个原子操作,业务成功之后,消息日志必定是存在的。解决了前两种方式遇到的问题。如果我们的业务系统比较单一,可以采用这种方式。

对于微服务化的情况,上面这种方式不是太好,每个服务都需要上面的操作;也不利于扩展。

6. 投递消息过程:方式四

增加一个消息服务消息库,负责消息的落库、将消息发送投递到mq。

  • step1:开启本地事务
  • step2:生成购物订单
  • step3:当前事务库插入一条日志:生成一个唯一的业务id(bus_id),将bus_id和订单关联起来保存到当前事务所在的库中
  • step4:调用消息服务:携带bus_id,将消息先落地入库,此时消息的状态为待发送状态,返回消息id(msg_id)
  • step5:提交本地事务
  • step6:如果上面都成功,调用消息服务,将消息投递到mq中;如果上面有失败的情况,则调用消息服务取消消息的发送

能想到上面这种方式,已经算是有很大进步了,我们继续分析一下可能存在的问题:

  1. 系统中增加了一个消息服务,商品下单操作依赖于该服务,业务对该服务依赖性比较高,当消息服务不可用时,整个业务将不可用。
  2. 若step6失败,消息将处于待发送状态,此时业务方需要提供一个回查接口(通过bus_id查询),验证业务是否执行成功;消息服务需新增一个定时任务,对于状态为待发送状态的消息做补偿处理,检查一下业务是否处理成功;从而确定消息是投递还是取消发送
  3. step4依赖于消息服务,如果消息服务性能不佳,会导致当前业务的事务提交时间延长,容易产生死锁,并导致并发性能降低。我们通常是比较忌讳在事务中做远程调用处理的,远程调用的性能和时间往往不可控,会导致当前事务变为一个大事务,从而引发其他故障。

7. 投递消息过程:方式五

在以上方式中,我们继续改进,进而出现了更好的一种方式:

  • step1:生成一个全局唯一业务消息id(bus_msg_id),调用消息服务,携带bus_msg_id,将消息先落地入库,此时消息的状态为待发送状态,返回消息id(msg_id)
  • step2:开启本地事务
  • step3:生成购物订单
  • step4:当前事务库插入一条日志(将step3中的业务和bus_msg_id关联起来)
  • step5:提交本地事务
  • step6:分2种情况:如果上面都成功,调用消息服务,将消息投递到mq中;如果上面有失败的情况,则调用消息服务取消消息的发送

若step6失败,消息将处于待发送状态,此时业务方需要提供一个回查接口(通过bus_msg_id查询),验证业务是否执行成功;

消息服务需新增一个定时任务,对于状态为待发送状态的消息做补偿处理,检查一下业务是否处理成功;从而确定消息是投递还是取消发送。

方式五和方式四对比,比较好的一个地方:将调用消息服务,消息落地操作,放在了事务之外进行,这点小的改进其实算是一个非常好的优化,减少了本地事务的执行时间,从而可以提升并发量,阿里有个消息中间件RocketMQ就支持方式5这种,大家可以去用用。

8. 关于消息消费的一些问题

如何解决重复消费的问题?

消费者轮询从mq server中拉取消息,然后进行消费。

消息消费者消费消息的过程

  • step1:从mq中拉取消息
  • step2:执行本地业务,比如增加积分操作
  • step3:消费完毕之后,将消息从mq中删掉

当step2成功,step3失败之后,这个消息会再次从mq中拉取出来,会出现重复消费的问题,所以我们需要考虑消费的幂等性,同一条消息多次消费和一次消费产生的结果应该是一致的,关于幂等性是另外一个课题,下次会详说。

分布式事务解决方案之最大努力通知型

1. 支付宝充值案例

假如我们自己有一个电商系统,支持用户使用支付宝充值,流程如下:

2. 用户支付流程(是一个同步的过程)

  1. 用户在浏览器发起充值请求->电商服务
  2. 电商服务生成充值订单,状态为0:待支付(0:待支付、100:支付成功、200:支付失败)
  3. 电商服务携带订单信息请求支付宝,生成支付宝订单,组装支付宝支付请求地址(订单信息、支付成功之后展示给用户的页面return_url、支付异步通知地址notify_url),将组装的信息返回给用户
  4. 用户浏览器跳转至支付宝支付页面,确认支付
  5. 支付宝携带支付结果同步回调return_url,return_url将支付结果展示给用户

3. 支付宝将支付结果异步通知给商户

用户支付流程完毕之后,此时支付宝中支付订单已经支付完毕,但电商中的充值订单状态还是0(待支付),此时支付宝会通过异步的方式将支付结果通知给notify_url,通知的过程中可能由于网络问题,导致支付宝通知失败,此时支付宝会通过多次衰减式的重试,尽最大努力将结果通知给商户,这个过程就是最大努力通知型。

商户接收到支付宝通知之后,通过幂等性的方式对本地订单进行处理,然后告知支付宝,处理成功,之后支付宝将不再通知。

4. 什么是衰减式的通知?

比如支付宝最大会尝试通知100次,每次通知时间间隔会递增。比如第1次失败之后,隔10s进行第2次通知,第2次失败之后,隔30s进行第三次通知,间隔时间依次递增的方式进行通知。

5. 如果支付宝一直通知不成功怎么办?

商户可以主动去调用支付宝的查询接口,查询订单的支付状态。

6. 为什么需要进行异步通知?

用户支付过程中,不是有个return_url么?支付宝支付成功之后会携带支付结果同步调用这个地址,那么商户直接在这个return_url中去处理一下本地订单状态不就可以了么?这种做法可以,但是有可能用户的网络不好,调用return_url失败了,此时还得依靠异步通知notify_url的方式将支付结果告知商户。

7. 最大努力通知型用在什么场景?

分布式事务中,不能立即知道调用结果的,被调方业务处理耗时可能比较长,被调方业务处理完毕之后,可以采用最大努力通知的方式将结果通知给调用方。

8. 最大努力通知型要有补偿机制

被调方会尽最大努力将结果通知给调用方,极端情况下有失败的可能,此时被调方需提供查询接口。

调用方对于长时间不知道结果的业务,可以主动去被调方查询,然后进行处理。

9. 不需要通知,主动去查可以么?

可以,被调方会提供查询接口,调用方主动去查询的方式完全是可以知道结果的,不过采用通知的方式实时性更高的一些。

被调方成功之后,会立即通知调用方,但是调用方主动采用查询的方式,那么什么时候查询呢?这个度不好把握,所以两则结合更好。

10. 分布式事务对比分析

在学习各种分布式事务的解决方案后,我们了解到各种方案的优缺点:

2PC最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其阻塞机制和最差时间复杂度高, 因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长 (long-running transactions) 的分布式服务中。

如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。典型的使用场景:满,登录送优惠券等。

可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等。

最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果通知等。

  2PC TCC 可靠消息 最大努力通知
一致性 强一致性 最终一致 最终一致 最终一致
吞吐量
实现复杂度

11. 总结

在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿分布式事务与单机事务ACID做对比。

无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部