一篇了解分布式事务

事务回顾

文章目录

1.什么是事务?

事务是逻辑上的一组整体操作,要么都执行,要么都不执行.

例如生活中转账案例、发红包、 支付宝向余额宝转账

2.事务的特性(ACID)

在这里插入图片描述

原子性:原子最小单位,不能分割,不能单独运行,要么成功要么失败。
一致性:例如能量守恒。
隔离性:多个并发访问时,一个事物的执行不能受其他事务的干扰。隔离级别解决问题。
持久性:事务一旦提交,它对数据库的修改是永久的。

3.事务没有隔离级别的问题

在这里插入图片描述
在这里插入图片描述

脏读:事务a读取了事务b未提交(commit)的数据,然后把他进行修改的操作,如果这时候事务b恰巧进行了回滚的操作,那么事务a读取到的就是脏数据,也就是所谓脏读。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

不可重复读:是指事务a读取到了事务b已提交的数据,两次查询的结果不一致。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

幻读:事务a读取到事务b提交的新增数据,幻读一般发生在统计数据中。
在这里插入图片描述

4.数据库事务隔离级别

常见的事务隔离级别有四种,从低到高依次是:

读未提交(Read uncommitted)
读已提交(Read committed)
可重复读(Repeatable read)
串行化(Serializable)

在这里插入图片描述

事务的隔离级别越低,可能出现的并发异常越多,但通常而言系统能提供的并发能力就越强。

读未提交(Read uncommitted),B事务可以读取到未提交的A事务中操作的数据,会导致脏读,为解决脏读,引入读已提交。

读已提交(Read committed),B事务要等A事务提交后才能读取到A事务中操作的数据,但是依然会导致不可重复读的问题(即B事务在A事务提交前后读取的数据不一致)。

可重复读(Repeatable read),一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,Mysql是通过快照版本来实现可重复读的,即事务开始时就拿到了一个版本号,别的事务操作的数据版本号不一样,就不会影响当前事务重复读取的数据,但是会看不到别的事务提交的数据。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。

序列化(Serializable),最高的事务隔离级别,事务串行化顺序执行,但是效率低下,比较耗数据库性能,一般不使用。
在这里插入图片描述

5.Spring事务两种实现方式

在这里插入图片描述

6.那么什么是事务属性呢?

6.1.拜神

在这里插入图片描述

spring事务领头人叫Juergen Hoeller,于尔根·糊了…先混个脸熟哈,他写了几乎全部的spring事务代码。读源码先拜神,掌握他的源码的风格,读起来会通畅很多。

6.1.事务的定义

事务(Transaction)是数据库区别于文件系统的重要特性之一。目前国际认可的数据库设计原则是ACID特性,用以保证数据库事务的正确执行。Mysql的innodb引擎中的事务就完全符合ACID特性。

spring对于事务的支持,分层概览图如下:
在这里插入图片描述

6.2.事务的特性

在这里插入图片描述
要保证事务的ACID特性,spring给事务定义了6个属性,对应于声明式事务注解(org.springframework.transaction.annotation.Transactional)@Transactional(key1=,key2=…)

隔离级别: 为了解决数据库容易出现的问题,分级加锁处理策略。 对应注解中的属性isolation
超时时间: 定义一个事务执行过程多久算超时,以便超时后回滚。可以防止长期运行的事务占用资源.对应注解中的属性timeout
是否只读:表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务.对应注解中的属性readOnly
传播机制: 对事务的传播特性进行定义,共有7种类型。对应注解中的属性propagation
回滚机制:定义遇到异常时回滚策略。对应注解中的属性rollbackFor、noRollbackFor、rollbackForClassName、noRollbackForClassName

在这里插入图片描述

6.3.七个传播特性

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

分布式理论基础

通过前面的学习,我们了解到了分布式事务的基础概念。与本地事务不同的是,分布式系统之所以叫分布式,是因 为提供服务的各个节点分布在不同机器上,相互之间通过网络交互。不能因为有一点网络问题就导致整个系统无法 提供服务,网络因素成为了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持,接下来,我 们先来学习一下分布式事务的CAP理论。

分布式系统理论基础

在讲解分布式事务控制解决方案之前需要先学习一些基础理论,通过理论知识指导我们确定分布式事务控制的目 标,从而帮助我们理解每个解决方案。

1.CAP理论(定理)

1.1.理解CAP(定理)

CAP是 ConsistencyAvailabilityPartition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。
在这里插入图片描述

下边我们分别来解释:

为了方便对CAP理论的理解,我们结合电商系统中的一些业务场景来理解CAP。
如下图,是商品信息管理的执行流程:
在这里插入图片描述
整体执行流程如下:

  1. 商品服务请求主数据库写入商品信息(添加商品、修改商品、删除商品)
  2. 主数据库向商品服务响应写入成功。
  3. 商品服务请求从数据库读取商品信息。

C - Consistency:

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

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

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

如何实现一致性?

  1. 写入主数据库后要将数据同步到从数据库。 主从复制!
  2. 写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。

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

  1. 由于存在数据同步的过程,写操作的响应会有一定的延迟。
  2. 为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
  3. 如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。

A - Availability :

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

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

  1. 从数据库接收到数据查询的请求则立即能够响应数据查询结果。
  2. 从数据库不允许出现响应超时或响应错误。

如何实现可用性?

  1. 写入主数据库后要将数据同步到从数据库。
  2. 由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
  3. 即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。

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

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

P - Partition tolerance :

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

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

  1. 主数据库向从数据库同步数据失败不影响读写操作。
  2. 其一个结点挂掉不影响另一个结点对外提供服务。

如何实现分区容忍性?

  1. 尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。
  2. 添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。

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

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

1.2.CAP组合方式

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

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

比如:

下图满足了P即表示实现分区容忍:
在这里插入图片描述

本图分区容忍的含义是:

  1. 主数据库通过网络向从数据同步数据,可以认为主从数据库部署在不同的分区,通过网络进行交互。
  2. 当主数据库和从数据库之间的网络出现问题不影响主数据库和从数据库对外提供服务。
  3. 其一个结点挂掉不影响另一个结点对外提供服务。
    如果要实现C则必须保证数据一致性,在数据同步的时候为防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败从数据库要返回错误信息或超时信息。

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

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

1.2.2.CAP有哪些组合方式呢?

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

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

Eureka注册中心:AP系统

Nacos注册中心:CP系统

Zookeeper: CP系统

Redis哨兵模式 CP系统

例如:上边的商品管理,完全可以实现AP,前提是只要用户可以接受所查询的到数据在一定时间内不是最新的即可。
通常实现AP都会保证最终一致性,后面讲的BASE理论就是根据AP来扩展的,一些业务场景 比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可。

2)CP:放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致,又比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。

3)CA:放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,我们最常用的关系型数据就满足了CA。

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

在这里插入图片描述

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

1.2.3. 总结

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

2.BASE理论

eBay 的架构师 Dan Pritchett 源于对大规模分布式系统的实践总结,在 ACM 上发表文章提出 BASE 理论,BASE 理论是对 CAP 理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。

基本可用(Basically Available): 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

软状态(Soft State): 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。MySQL Replication 的异步复制也是一种体现。

审核中… 退款中… 转账中…

最终一致性(Eventual Consistency): 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

3.ACID 和 BASE 的区别与联系

ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。

ACID 和 BASE 代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。

分布式事务解决方案

1.分布式事务概念

1.1.问题引入

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

前阵子从支付宝转账1万块钱到**余额宝,**这是日常生活的一件普通小事,但作为互联网研发人员的职业病,我就思考支付宝扣除1万之后,如果系统挂掉怎么办,这时余额宝账户并没有增加1万,数据就会出现不一致状况了。

招行 ----------转账--------> 建行 事务问题

上述场景在各个类型的系统中都能找到相似影子,比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?!

在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增加一条记录外,还得去商家账户表中找到这个商家并扣除广告费吧,怎么保证?!等等,相信大家或多或多少都能碰到相似情景。

本质上问题可以抽象为:

当一个表数据操作成功后,怎么保证另一个表的数据也必须要操作成功。当然啦,这两个数据表不在一个数据源中

conn.setAutoCommit(false);

conn.commit();

conn.rollback();

1.2.加入没有分布式事务

在一系列微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:
在这里插入图片描述
上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。
正常情况下,两个数据库各自更新成功,两边数据维持着一致性。

在这里插入图片描述
但是,在非正常情况下,有可能库存的扣减完成了,随后的订单记录却因为某些原因插入失败。这个时候,两边数据就失去了应有的一致性。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.3.小结

事务: 指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行.

本地事务: SqlSessionfactory -DataSources(Connection)-> 一个数据库范围类事务管理.

分布式事务:跨了多个数据库事务管理,在微服务架构每个服务都有自己数据库,在微服务架构中必然要用到分布式事务.

2.柔性事务 vs 刚性事务

刚性事务是指严格遵循ACID原则的事务, 例如单机环境下的数据库事务.

柔性事务是指遵循BASE理论的事务, 通常用在分布式环境中, 常见的实现方式有:

①两阶段提交(2PC)

②TCC补偿型提交

③基于消息的异步确保型

④最大努力通知型

通常对本地事务采用刚性事务, 分布式事务使用柔性事务.

3.两阶段提交

分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务的实现有很多种,最具有代表性的是由Oracle Tuxedo系统提出的XA分布式事务协议。

XA协议包含两阶段提交(2PC),这里我们重点介绍两阶段提交的具体过程

3.1.两阶段提交(2PC)

两阶段提交(Two Phase Commit, 2PC), 具有强一致性, 是系统的一种典型实现.两阶段提交, 常见的标准是XA, JTA等. 例如Oracle的数据库支持XA.
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在魔兽世界这款游戏中,副本组团打BOSS的时候,为了更方便队长与队员们之间的协作,队长可以发起一个“就位确认”的操作:
在这里插入图片描述
当队员收到就位确认提示后,如果已经就位,就选择“是”,如果还没就位,就选择“否”。
在这里插入图片描述
当队长收到了所有人的就位确认,就会向所有队员们发布消息,告诉他们开始打BOSS。
在这里插入图片描述
相应的,在队长发起就位确认的时候,有可能某些队员还并没有就位:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以上就是魔兽世界当中组团打BOSS的确认流程。这个流程和XA分布式事务协议的两阶段提交非常相似。

那么XA协议究竟是什么样子呢?在XA协议中包含着两个角色:事务协调者事务参与者。让我们来看一看他们之间的交互流程:

3.1.1.第一阶段

在这里插入图片描述
在XA分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。

在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo Log和Redo Log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。

当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。

3.3.2.第二阶段

在这里插入图片描述
在XA分布式事务的第二阶段,如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求。

接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。

当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。

3.3.3.失败的场景

第一阶段

在这里插入图片描述
第二阶段
在这里插入图片描述
在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。

于是在第二阶段,事务协调节点向所有的事务参与者发送Abort请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照Undo Log来进行。

以上就是XA两阶段提交协议的详细过程

3.2.小结

在这里插入图片描述

  • 1) 我们的应用程序(client)发起一个开始请求到TC;
  • 2) TC先将消息写到本地日志,之后向所有的Si发起消息。以支付宝转账到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应账目扣款1万,TC给B的prepare消息是通知余额宝数据库相应账目增加1w。为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证 的效果,如果没有本地日志(凭证),出问题容易死无对证;
  • 3) Si收到消息后,执行具体本机事务,但不会进行commit,如果成功返回,不成功返回。同理,返回前都应把要返回的消息写到日志里,当作凭证。
  • 4) TC收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。

注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一Si从故障中恢复后,先检查本机的日志,如果已收到,则提交,如果则回滚。如果是,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在阶段Si就崩溃了,因此需要回滚。

现如今实现基于两阶段提交的分布式事务也没那么困难了,

如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/)来快速实现。

LCN 基于2PC封装的框架 国产开源

3.3.两阶段提交(2PC)不足

不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?

3.3.1.性能问题

XA协议遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。

3.2.2.协调者单点故障问题

事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。

3.2.2.丢失消息导致的不一致问题

在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。

4.TCC (Try-Confirm-Cancle TCC两阶段补偿性方案)

TCC事务的出现正是为了解决应用拆分带来的跨应用业务操作原子性的问题。当然,由于常规的XA事务(2PC,2 Phase Commit, 两阶段提交)性能上不尽如人意,也有通过TCC事务来解决数据库拆分的使用场景。

4.1.TCC的机制

个参与者需要实现3个操作:Try、Confirm 和 Cancel,3个操作对应2个阶段,

Try 方法是一阶段的资源检测和预留阶段


Confirm 和 Cancel 对应二阶段的提交和回滚。

在这里插入图片描述

图中,事务开启的时候,由发起方去触发一阶段的方法,然后根据各个参与者的返回状态,决定二阶段是调 Confirm 还是 Cancel 方法。

我们先套一个业务场景进去,如下图所示
在这里插入图片描述

那页面点了支付按钮,调用支付服务,那我们后台要实现下面三个步骤

[1] 订单服务-修改订单状态
[2] 账户服务-扣减金钱
[3] 库存服务-扣减库存

达到事务的效果,要么一起成功,要么一起失败!就要采取TCC分布式事务方案!
在这里插入图片描述

TCC又可以被称为两阶段补偿事务,第一阶段try只是预留资源,第二阶段要明确的告诉服务提供者,这个资源你到底要不要,对应第二阶段的confirm/cancel,用来清除第一阶段的影响,所以叫补偿型事务。

再打个比方,说TCC太高大上是吧,讲RM中的prepare、commit、rollback接口,总知道吧。可以类比的这么理解
在这里插入图片描述

那差别在哪呢?
rollback、commit、prepare,站在开发者层面是感知不到的,数据库帮你做了资源的操作!
而try、confirm、cancel,站在开发者层面是能感知到的,这三个方法的业务逻辑,即对资源的操作,开发者是要自己去实现的!
好,下面套入我们的场景,怎么做呢。比如,你的订单服务中本来只有一个接口

//修改代码状态
orderClient.updateStatus()

都要拆为三个接口,即

orderClient.tryUpateStatus()// try尝试修改
orderClient.confirmUpateStatus()// 确认修改
orderClient.cancelUpateStatus()// 回滚修改

注意了:面试官如果问你,TCC有什么缺点?这就是很严重的缺点,对代码入侵性大!每套业务逻辑、都要按try(请求资源)、confirm(操作资源)、cancel(取消资源),拆分为三个接口!

具体每个阶段,每个服务业务逻辑是什么样的呢?

假设,库存数量本来是50,那么可销售库存也是50。账户余额为50,可用余额也为50。用户下单,买了1个单价为1元的商品。流程如下:

Try阶段:

订单服务:修改订单的状态为【支付中】
账户服务:账户余额不变,可用余额减1,然后将1这个数字冻结在一个单独的字段里
库存服务:库存数量不变,可销售库存减1,然后将1这个数字冻结在一个单独的字段里

confirm阶段

订单服务:修改订单的状态为【支付完成】
账户服务:账户余额变为(当前值减冻结字段的值),可用余额不变(Try阶段减过了),冻结字段清0。
库存服务:库存变为(当前值减冻结字段的值),可销售库存不变(Try阶段减过了),冻结字段清0。

cancel阶段

订单服务:修改订单的状态为【未支付】
账户服务:账户余额不变,可用余额变为(当前值加冻结字段的值),冻结字段清0。
库存服务:库存不变,可销售库存变为(当前值加冻结字段的值),冻结字段清0。

伪代码
接下来从代码程序来说明,为了便于演示,将入参略去。
本来,你支付服务的代码是长下面这样的
在这里插入图片描述
那么,用上TCC模型后,代码变成下面这样
在这里插入图片描述
注意了,这种写法其实严格上来说,不是不行。看你业务场景,因为存在一些瑕疵,看你自己有没办法接受

(1)cancel或者confirm出现异常了,你怎么处理?
例如在cancel阶段执行如下三行代码

orderClient.cancelUpdateStatus();
accountClient.cancelDecrease();
repositoryClient.cancelDecrease();

你第二行出现异常了,第三行没跑就退出了,怎么办?你要对此进行业务补偿!

(2)大量逻辑重复
你看啊,我们的执行架构其实是这样的

try{
    xxclient.try();
}catch(Throwable t){
    xxclient.cancel();
    throw t;
}
xxclient.confirm();

有没办法让这个架子交给框架去执行,我们告诉框架,你在每个阶段要执行哪些方法就好!

因此,需要引入TCC分布式事务框架,事务的Try、Confirm、Cancel三个状态交给框架来感知!你只要告诉框架,Try要执行啥,Confirm要执行啥,Cancel要执行啥!如果Cancel过程出现异常了,框架有内部的补偿措施给你恢复数据!

以分布式tcc框架hmily为例,如果出现cancel异常或者confirm异常的情况,在try阶段会保存好日志,Hmily有内置的调度线程池来进行恢复,不用担心。
那hmily,怎么感知状态的呢?也很简单,就是切面编程,核心逻辑如下几行
在这里插入图片描述
我们在使用过程中,只要通过@Tcc注解告诉框架confirm方法执行啥,cancel方法执行啥即可!其他的交给框架帮你处理!

4.2.使用场景:
  严格一致性
  执行时间短
  实时性要求高

5.消息队列异步确保型

一个业务场景,也是很常见的一个异步调用场景:

支付宝往余额宝转钱

即将服务A假设为支付宝,服务B假设为余额宝。于是呢,我们的支付宝往余额宝转100块钱是怎么做的呢?特别容易,借助消息队列即可,如下图所示
在这里插入图片描述
一致性解决

OK,上面这一版有一个致命的问题!如下所示

事务开始

(1)给支付宝账户zhangsan,100--------------------------  mysql
(2)(给余额宝账户zhangsan,100)封装为消息,发送给消息队列 ---------------------------  rabbitmQ 

事务结束

敢问你,如何保证第一步和第二步是在同一个事务里完成的。换句话说,第一步操作的是数据库,第二步操作的是一个消息队列,你如何保证这两步之间的一致性?

记住了,任何涉及到数据库和中间件之间的业务逻辑操作,都需要考虑二者之间的一致性。比如,你先操作了数据库,再操作缓存,数据库和缓存之间一致性如何解决?好吧,改变思路,加一张事务表,如下图所示
在这里插入图片描述
注意了,此时事务的内容为

事务开始

(1)给支付宝账户zhangsan,100(2)给事件表插入一条记录

事务结束

此时是对同一数据库的两张表操作,因此可以用数据库的事务进行保证。另外,起一个定时程序,定时扫描事务表,发现一个状态为’UNFINISHED’的事件,就进行封装为消息,发送到消息中间件,然后将状态改为’FINISHED’.

幂等性解决

注意了,这一版还存在一个幂等性问题!
仔细看,定时程序做了如下三个操作

(1)定时扫描事务表,发现一个状态为'UNFINISHED'的事件
(2)将事件信息,封装为消息,发送到消息中间件
(3)将事件状态改为'FINISHED'

OK,假设在步骤(2)的时候,发送完消息体,还未执行步骤(3),定时程序阵亡了!然后重启定时程序,发现刚那个事务的状态依然为’UNFINISHED’,因此重新发送。这样,就会出现重复消费问题。因此,幂等性也是需要保证的!

在消费者端,也维护一个带主键的表,可以选txid为主键,如下图所示
在这里插入图片描述
如果一旦出现重复消费,则在事务里直接报出主键冲突错误,从而保证了幂等性!

  1. 保证消息发布到消息队列,如果生产者回调发现消息发送失败,重试几次,实战发送不出去日志记录!
  2. 保证消费者能正常消费消息,同时保证消息幂等性问题!消费消息可以重试消费几次,转入到死信交换机和死信队列进行后续处理!

6.分布式事务常见框架选择

6.1.GTS–分布式事务解决方案

GTS是一款分布式事务中间件,由阿里巴巴中间件部门研发,可以为微服务架构中的分布式事务提供一站式解决方案。

GTS的核心优势:
性能超强
GTS通过大量创新,解决了事务ACID特性与高性能、高可用、低侵入不可兼得的问题。单事务分支的平均响应时间在2ms左右,3台服务器组成的集群可以支撑3万TPS以上的分布式事务请求。

应用侵入性极低
GTS对业务低侵入,业务代码最少只需要添加一行注解(@TxcTransaction)声明事务即可。业务与事务分离,将微服务从事务中解放出来,微服务关注于业务本身,不再需要考虑反向接口、幂等、回滚策略等复杂问题,极大降低了微服务开发的难度与工作量。

完整解决方案
GTS支持多种主流的服务框架,包括EDAS,Dubbo,Spring Cloud等。
有些情况下,应用需要调用第三方系统的接口,而第三方系统没有接入GTS。此时需要用到GTS的MT模式。GTS的MT模式可以等价于TCC模式,用户可以根据自身业务需求自定义每个事务阶段的具体行为。MT模式提供了更多的灵活性,可能性,以达到特殊场景下的自定义优化及特殊功能的实现。

容错能力强
GTS解决了XA事务协调器单点问题,实现真正的高可用,可以保证各种异常情况下的严格数据一致。

但是不开源!!

6.2.TX-LCN–分布式事务解决方案

介绍:“LCN并不生产事务,LCN只是本地事务的协调者”

LCN分布式事务框架的核心功能是对本地事务的协调控制,框架本身并不创建事务,只是对本地事务做协调控制。因此该框架与其他第三方的框架兼容性强,支持所有的关系型数据库事务,支持多数据源,支持与第三方数据库框架一块使用(例如 sharding-jdbc),在使用框架的时候只需要添加分布式事务的注解即可,对业务的侵入性低。LCN框架主要是为微服务框架提供分布式事务的支持,在微服务框架上做了进一步的事务机制优化,在一些负载场景上LCN事务机制要比本地事务机制的性能更好,4.0以后框架开方了插件机制可以让更多的第三方框架支持进来。

特点:

 ①支持各种基于spring的db框架
 ②兼容SpringCloud、Dubbo、motan
 ③使用简单,低依赖,代码完全开源
 ④基于切面的强一致性事务框架
 ⑤高可用,模块可以依赖RPC模块做集群化,TxManager也可以做集群化
 ⑥支持本地事务和分布式事务共存
 ⑦支持事务补偿机制,增加事务补偿决策提醒
 ⑧添加插件拓展机制

选择 GTS比较N但是不开源,所以选择tx-lcn

6.3.seata

2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案。Fescar 的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。

Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。

为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为 Seata,意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。

spring cloud ------> txlcn

spring cloud alibaba —> seata

Seata 融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下丰富的实践经验,但要实现适用于所有的分布式事务场景的愿景,仍有很长的路要走。因此,我们决定建立一个完全中立的分布式事务组织,希望更多的企业、开发者能够加入我们,一起打造 Seata。

在这里插入图片描述
在这里插入图片描述

LCN分布式事务解决方案

1.LCN是什么

LCN是国产开源的分布式事务处理框架。LCN即:lock(锁定事务单元)、confirm(确认事务模块状态)、notify(通知事务)。

官网:http://www.txlcn.org/zh-cn/
在这里插入图片描述

2.首先介绍3.0与4.0之前的差异

2.1.地址

在这里插入图片描述

2.2.添加升级如下功能

在这里插入图片描述
(1)3.0虽然有事务补偿机制,但4.0在此基础上不仅添加事务补偿机制的策性,还添加了管理的后台可以看到补偿的数据;同时也添加了一个回调地址,可以在补偿之前可以最先知道这次补偿的数据,也可以为我们的框架使用者提供一个决策权。

(2)同4.0时添加的插件扩展机制,也就是说他更加开放了,他可以可以容纳更多的rpc框架,也可以更多的支持db框架,比如mongodb、redis,还有将来一些框架,如ES等等。

3.LCN4.0原理

3.1.架构介绍

TXManager事务协调服务器,依赖于Redis和Eureka,Redis中存储事务的信息和事务补偿的信息,Eureka负责完成服务注册与发现,方便TXManager搭建集群!

在这里插入图片描述

有图可得,lcn是通过nginx作为负载均衡的转发,也就是作为Txmanager的负载均衡的一个转发服务器;然后再是我们的TxManager,也就是事务管理器,然后事务管理器依赖两个服务一个是redis服务一个是Eureka服务集群;Eureka集群是用于我们TxManager之间的相互服务发现。redis是用于存放我们事务组的信息以及补偿的信息。然后模块A与模块B他们都需要去配置上我们TxClient的包架构(代码的包架构);来支持我们的LCN框架,以及他们的数据库。

3.2.核心步骤

LCN正常执行序列图(来源于官方):
在这里插入图片描述
LCN异常执行序列图(来源于官方):
在这里插入图片描述

3.3.事务协调机制

在这里插入图片描述

如图:假设服务已经执行到关闭事务组的过程,那么接下来作为一个模块执行通知给TxManager,然后告诉他本次事务已经完成。那么如图中Txmanager下一个动作就是通过事务组的id,然后获取到本次事务组的事务信息;然后查看一下对应有那几个模块参与,然后如果是有A/B/C三个模块;那么对应的对三个模块做通知、提交、回滚。

那么提交的时候是提交给谁呢?

是提交给了我们的TxClient模块。然后TxCliient模块下有一个连接池,就是框架自定义的一个连接池(如图DB连接池);这个连接池其实就是在没有通知事务之前一直占有着这次事务的连接资源,就是没有释放。但是他在切面里面执行了close方法。在执行close的时候。如果需要(TxManager)分布式事务框架的连接。他被叫做“假关闭”,也就是没有关闭,只是在执行了一次关闭方法。实际的资源是没有释放的。这个资源是掌握在LCN的连接池里的。

然后当TxManager通知提交或事务回滚的时候呢?

TxManager会通知我们的TxClient端。然后TxClient会去执行相应的提交或回滚。提交或回滚之后再去关闭连接,然后在返回给DB连接池。这就只事务的协调机制。说白了就是代理DataSource的机制;相当于是拦截了一下连接池,控制了连接池的事务提交。

LCN事务控制原理是由事务模块TxClient下的代理连接池与TxManager的协调配合完成的事务协调控制。

TxClient的代理连接池实现了javax.sql.DataSource接口,并重写了close方法,事务模块在提交关闭以后TxClient连接池将执行"假关闭"操作,等待TxManager协调完成事务以后在关闭连接。

4.Spring Cloud 整合LCN

4.1.下载LCN工程

在LCN的github下载:https://github.com/codingapi/tx-lcn/

4.2.配置tx-manager事务协调器

修改其属性文件: (修改下载事务协调服务器的端口、接入的服务注册中心、使用的redis库等的集群或单点配置)

#######################################txmanager-start#################################################
#服务端口
server.port=8899

#tx-manager不得修改
spring.application.name=tx-manager

spring.mvc.static-path-pattern=/**
spring.resources.static-locations=classpath:/static/
#######################################txmanager-end#################################################


#zookeeper地址
#spring.cloud.zookeeper.connect-string=127.0.0.1:2181
#spring.cloud.zookeeper.discovery.preferIpAddress = true

#eureka 地址
eureka.client.service-url.defaultZone=http://eurekaserver1:8081/eureka/,http://eurekaserver2:8082/eureka/,http://eurekaserver3:8083/eureka/
eureka.instance.prefer-ip-address=true

#######################################redis-start#################################################
#redis 配置文件,根据情况选择集群或者单机模式

##redis 集群环境配置
##redis cluster
#spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
#spring.redis.cluster.commandTimeout=5000

##redis 单点环境配置
#redis
#redis主机地址
spring.redis.host=192.168.6.211
#redis主机端口
spring.redis.port=6379
#redis链接密码
spring.redis.password=
spring.redis.pool.maxActive=10
spring.redis.pool.maxWait=-1
spring.redis.pool.maxIdle=5
spring.redis.pool.minIdle=0
spring.redis.timeout=0
#####################################redis-end###################################################

#######################################LCN-start#################################################
#业务模块与TxManager之间通讯的最大等待时间(单位:秒)
#通讯时间是指:发起方与响应方之间完成一次的通讯时间。
#该字段代表的是Tx-Client模块与TxManager模块之间的最大通讯时间,超过该时间未响应本次请求失败。
tm.transaction.netty.delaytime = 5

#业务模块与TxManager之间通讯的心跳时间(单位:秒)
tm.transaction.netty.hearttime = 15

#存储到redis下的数据最大保存时间(单位:秒)
#该字段仅代表的事务模块数据的最大保存时间,补偿数据会永久保存。
tm.redis.savemaxtime=30

#socket server Socket对外服务端口
#TxManager的LCN协议的端口
tm.socket.port=9999

#最大socket连接数
#TxManager最大允许的建立连接数量
tm.socket.maxconnection=100

#事务自动补偿 (true:开启,false:关闭)
# 说明:
# 开启自动补偿以后,必须要配置 tm.compensate.notifyUrl 地址,仅当tm.compensate.notifyUrl 在请求补偿确认时返回success或者SUCCESS时,才会执行自动补偿,否则不会自动补偿。
# 关闭自动补偿,当出现数据时也会 tm.compensate.notifyUrl 地址。
# 当tm.compensate.notifyUrl 无效时,不影响TxManager运行,仅会影响自动补偿。
tm.compensate.auto=false

#事务补偿记录回调地址(rest api 地址,post json格式)
#请求补偿是在开启自动补偿时才会请求的地址。请求分为两种:1.补偿决策,2.补偿结果通知,可通过通过action参数区分compensate为补偿请求、notify为补偿通知。
#*注意当请求补偿决策时,需要补偿服务返回"SUCCESS"字符串以后才可以执行自动补偿。
#请求补偿结果通知则只需要接受通知即可。
#请求补偿的样例数据格式:
#{"groupId":"TtQxTwJP","action":"compensate","json":"{\"address\":\"133.133.5.100:8081\",\"className\":\"com.example.demo.service.impl.DemoServiceImpl\",\"currentTime\":1511356150413,\"data\":\"C5IBLWNvbS5leGFtcGxlLmRlbW8uc2VydmljZS5pbXBsLkRlbW9TZXJ2aWNlSW1wbAwSBHNhdmUbehBqYXZhLmxhbmcuT2JqZWN0GAAQARwjeg9qYXZhLmxhbmcuQ2xhc3MYABABJCo/cHVibGljIGludCBjb20uZXhhbXBsZS5kZW1vLnNlcnZpY2UuaW1wbC5EZW1vU2VydmljZUltcGwuc2F2ZSgp\",\"groupId\":\"TtQxTwJP\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo1\",\"state\":0,\"time\":36,\"txGroup\":{\"groupId\":\"TtQxTwJP\",\"hasOver\":1,\"isCompensate\":0,\"list\":[{\"address\":\"133.133.5.100:8899\",\"isCompensate\":0,\"isGroup\":0,\"kid\":\"wnlEJoSl\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo2\",\"modelIpAddress\":\"133.133.5.100:8082\",\"channelAddress\":\"/133.133.5.100:64153\",\"notify\":1,\"uniqueKey\":\"bc13881a5d2ab2ace89ae5d34d608447\"}],\"nowTime\":0,\"startTime\":1511356150379,\"state\":1},\"uniqueKey\":\"be6eea31e382f1f0878d07cef319e4d7\"}"}
#请求补偿的返回数据样例数据格式:
#SUCCESS
#请求补偿结果通知的样例数据格式:
#{"resState":true,"groupId":"TtQxTwJP","action":"notify"}
tm.compensate.notifyUrl=http://ip:port/path

#补偿失败,再次尝试间隔(秒),最大尝试次数3次,当超过3次即为补偿失败,失败的数据依旧还会存在TxManager下。
tm.compensate.tryTime=30

#各事务模块自动补偿的时间上限(毫秒)
#指的是模块执行自动超时的最大时间,该最大时间若过段会导致事务机制异常,该时间必须要模块之间通讯的最大超过时间。
#例如,若模块A与模块B,请求超时的最大时间是5秒,则建议改时间至少大于5秒。
tm.compensate.maxWaitTime=5000
#######################################LCN-end#################################################

logging.level.com.codingapi=debug
4.3.启动事务协调者

启动事务协调者,让事务协调者注入进入eureka;(注意,配置中的redis等必须正常启动)
启动成功后,检查tx-manager协调者,见下图:
在这里插入图片描述

4.4 事务参与方配置

假定:事务参与方已经是正常运行的服务提供者。样例中的数据库是mysql,连接池采用druid;

4.4.1 pom文件引入LCN db插件和springcloud支持:
    <properties>
        <lcn.last.version>4.1.0</lcn.last.version>
    </properties>


        <!-- 引入LCN-->
        <dependency>
            <groupId>com.codingapi</groupId>
            <artifactId>transaction-springcloud</artifactId>
            <version>${lcn.last.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.codingapi</groupId>
            <artifactId>tx-plugins-db</artifactId>
            <version>${lcn.last.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
4.4.2 yml添加配置
tm:
  manager:
    url: http://127.0.0.1:8899/tx/manager/
注入数据库连接池
@Autowired
private Environment env;

@Bean
public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(env.getProperty("spring.datasource.url"));
    dataSource.setUsername(env.getProperty("spring.datasource.username"));//用户名
    dataSource.setPassword(env.getProperty("spring.datasource.password"));//密码
    dataSource.setInitialSize(2);
    dataSource.setMaxActive(20);
    dataSource.setMinIdle(0);
    dataSource.setMaxWait(50000);
    dataSource.setValidationQuery("SELECT 1");
    dataSource.setTestOnBorrow(false);
    dataSource.setTestWhileIdle(true);
    dataSource.setPoolPreparedStatements(false);
    return dataSource;
}

4.4.3 添加TxManagerTxUrlService到spring中

package com.mark.springcloud.service.impl;

import com.codingapi.tx.config.service.TxManagerTxUrlService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

/**
 * 添加从注册中心获取url;注意通过注解放入容器。
 */
@Service
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService{
    @Value("${tm.manager.url}")
    private String url;
    @Override
    public String getTxUrl() {
        return url;
    }
}
4.4.4 事务参与方服务:
package com.mark.springcloud.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.codingapi.tx.annotation.ITxTransaction;
import com.mark.springcloud.dao.DeptDao;
import com.mark.springcloud.entities.Dept;
import com.mark.springcloud.service.DeptService;
/**
 * 注意需要实现 ITxTransaction;
 */
@Service
public class DeptServiceImpl implements DeptService, ITxTransaction {
    @Autowired
    private DeptDao dao;

    //注意需要开启事务
    @Override
    @Transactional
    public boolean add(Dept dept) {
        boolean rtnValue = dao.addDept(dept);
        return rtnValue;
    }
}
4.4.5 启动事务参与方

启动spring boot应用。

4.5 事务发起方配置

正常情况下,一个服务一般即可能是事务的发起方也是事务的参与方。(在测试事务发起方、参与方都是同样配置。所以直接略过,只描述发起方特有代码)

4.5.1 参照样例,实现TxManagerHttpRequestService
package com.mark.springcloud.controller;

import com.codingapi.tx.netty.service.TxManagerHttpRequestService;
import com.lorne.core.framework.utils.http.HttpUtils;
import org.springframework.stereotype.Service;

/**
 * 常见TxManagerHttpRequestService重写get、post方法;
 */

@Service
public class TxManagerHttpRequestServiceImpl implements TxManagerHttpRequestService{

    @Override
    public String httpGet(String url) {
        System.out.println("httpGet-start");
        String res = HttpUtils.get(url);
        System.out.println("httpGet-end");
        return res;
    }

    @Override
    public String httpPost(String url, String params) {
        System.out.println("httpPost-start");
        String res = HttpUtils.post(url,params);
        System.out.println("httpPost-end");
        return res;
    }
}
4.5.2 事务发起方服务处理
package com.mark.springcloud.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.codingapi.tx.annotation.TxTransaction;
import com.mark.springcloud.entities.Dept;
import com.mark.springcloud.service.DeptClientService;

@RestController
public class DeptController_Consumer
{
    @Autowired
    private DeptClientService service;
    //@TxTransaction(isStart = true)注解修饰该方法为事务发起方,开启事务组。
    @TxTransaction(isStart = true)
    @RequestMapping(value = "/consumer/dept/add")
    public Object add(Dept dept)
    {
        Object rtnObj = this.service.add(dept);
        int x = (int)(Math.random()*10);
        //事务发起方随机数小于5时,抛出异常,则事务参与方事务会回滚。否则正常执行,事务参与方事务正常提交。
        if (x < 5) {
            int m = 1/0;
        }
        return rtnObj;
    }
}
4.5.3 启动事务发起方

启动spring boot 应用。

4.6.测试事务

调用事务发起方服务,事务正常受事务协调者控制,当发起方和参与方都正常执行无异常时,事务正常提交,否则回滚。

4.7.总结

Spring Cloud 集成LCN进行分布式事务控制使用简单,整个原理也很清晰。

2PC和3PC原理

1.分布式事物常见解决方案:

- 2PC两段提交协议
- 3PC三段提交协议(弥补两端提交协议缺点)
- TCC或者GTS(阿里)
- 消息中间件最终一致性
- 使用LCN解决分布式事物,理念“LCN并不生产事务,LCN只是本地事务的搬运工”。

2.两阶段提交(2PC)

两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议。

这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant)。

两个阶段:第一阶段:投票阶段 和第二阶段:提交/执行阶段

举例 订单服务A,需要调用 支付服务B 去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。

那么看2PC阶段是如何处理的

2.1.第一阶段:投票阶段

在这里插入图片描述
第一阶段主要分为3步

1)事务询问

协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应。

2)执行本地事务

各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我这边可以处理了/我这边不能处理”。.

3)各参与者向协调者反馈事务询问的响应

如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行,如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行。

第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。

2.2.第二阶段:提交/执行阶段(成功流程)

成功条件:所有参与者都返回Yes。
在这里插入图片描述
第二阶段主要分为两步

1)所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交

协调者 向 所有参与者 节点发出Commit请求.

2)事务提交

参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。

2.3.第二阶段:提交/执行阶段(异常流程)

异常条件:任何一个 参与者 向 协调者 反馈了 No 响应,或者等待超时之后,协调者尚未收到所有参与者的反馈响应。
在这里插入图片描述
异常流程第二阶段也分为两步

1)发送回滚请求

协调者 向所有参与者节点发出 RoollBack 请求.

2)事务回滚

参与者 接收到RoollBack请求后,会回滚本地事务。

4、2PC缺点
通过上面的演示,很容易想到2pc所带来的缺陷

1)性能问题

无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务 协调者 才会通知进行全局提交,

参与者 进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。

2)单节点故障

由于协调者的重要性,一旦 协调者 发生故障。参与者 会一直阻塞下去。尤其在第二阶段,协调者 发生故障,那么所有的 参与者 还都处于锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

2PC出现单点问题的三种情况

(1)协调者正常,参与者宕机

由于 协调者 无法收集到所有 参与者 的反馈,会陷入阻塞情况。

解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。

(2)协调者宕机,参与者正常

无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.

解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。

(3)协调者和参与者都宕机

发生在第一阶段: 因为第一阶段,所有参与者都没有真正执行commit,所以只需重新在剩余的参与者中重新选出一个协调者,新的协调者在重新执行第一阶段和第二阶段就可以了。

2)发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。也就是上面的第4步挂了,这是可能协调者还没有发送第4步就挂了。这种情形下,新的协调者重新执行第一阶段和第二阶段操作。

3)发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到协调者 发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了! 2PC 无法解决这个问题。

3.三阶段提交(3PC)

三阶段提交协议(3PC)主要是为了解决两阶段提交协议的阻塞问题,2pc存在的问题是当协作者崩溃时,参与者不能做出最后的选择。因此参与者可能在协作者恢复之前保持阻塞。三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。

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

1、引入超时机制。同时在协调者和参与者中都引入超时机制。

2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

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

3.1.CanCommit阶段

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

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

响应反馈 参与者 接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

3.2.PreCommit阶段

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

3.3.DoCommit阶段

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

4.总结

相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?

这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,

自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。

另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。

了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版

总结

分布式事务应用场景

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值