一、用户汇款场景

用户 A 的账户存有 2200 元,用户 B 的账户存有 1600 元。现在用户 A 给用户 B 汇款 200 元,正确的执行步骤为:

  1. A 用户:A存款 = 2200 - 200

  2. B 用户:B存款 = 1600 + 200

  • 如果上面的汇款步骤正确执行完,那么 A 用户现在的存款数是 2000 元, B 用户现在的存款数是 1800。

  • 如果上面的汇款步骤没有正确执行完呢?比如遇到了异常情况, A 用户存款数已经扣去了 200, B 用户的存款数却没有加上 200。那么现在 A 用户存款数是 2000 元,B 用户的存储数还是 1600 元。此时,A 用户存款数和 B 用户存款数都出现了错误。

A 用户汇款了存款数减少了,B 用户却没有收到汇款。这种情况一定会给用户造成困扰,不得不打电话去银行询问出了啥事情?

为什么会出现这种情况?

在汇款操作的步骤中,有 2 个步骤:

  1. A 用户汇款 2. B 用户收款

日常理解来看这 2 个步骤没多大问题,但是在计算机软件执行操作中来看,A 扣款和 B 收款是2个不同动作,所以 A 扣款动作可能成功也可能失败,B 同样如此。因为程序和网络有可能出现各种异常情况。

这种分步骤的日常操作,在软件中我们需要把它们当作一个整体来操作,操作结果是要么都成功,要么都失败。

上面汇款来说,都成功就是,A 扣款减少和 B 收款增加都成功。都失败就是,A 扣款减少和 B 收款增加都失败,A 回滚到原来存款数,B 存款数不增不减。(A 的存款数 + B 的存款数)= 总资金数,总资金数在汇款前后是不变的。

A扣款操作和B收款操作当作是一个整体操作,一个不可分割的原子操作。在计算机软件中,我们把这种操作叫做事务。

二、什么是事务

上面讨论的场景已经引出了什么是事务?

事务是将程序中的多个操作(比如多个读、写等)"糅合"在一起成为一个整体的逻辑操作单元,整个逻辑操作单元中的所有读写操作是一个执行的整体,整体被视为一个不可分割的操作,这个就称为事务。事务操作要么成功,要么失败(终止或回滚)。

如果事务操作失败了,不需要担心事务里的部分操作失败的情况,部分失败后可回滚,恢复到原来数据状态。

三、事务的 ACID 特性

通常说到数据库事务时,都会提到 ACID 这 4 个特性。这 4 个特性是 TheoHarder 和 Andreas Reuter 于1983年为精确描述数据库的容错机制而定义的。

但各家数据库系统实现的 ACID 又不尽相同。有些系统说自己提供事务时或”兼容ACID“时,其实我们无法确信它究竟提供了什么样的保证,需要仔细查看该系统文档或代码才知晓细节。

InnoDB 默认事务隔离级别是可重复读,不满足隔离性;Oracle 默认的事务隔离级别为读已提交,不满足隔离性。因为有时完全满足就可能导致性能问题,有一个取舍平衡。

下面看看 ACID 具体含义:

  • C(Consistency) 一致性:当事务开始和结束时,数据处于一致的状态。比如上面汇款场景,总资金数(数据)在汇款前后是不变的,保持了前后一致。

    有的人说一致性是 ACID 的目的,只要保证了原子性、隔离性、持久性,也就保证了数据的一致性。

    数据库提供了某些一致性的约束,比如主键 ID,唯一索引等。对于业务数据一致性,数据库并没有提供很好的约束,业务数据的一致性需要应用程序来保证。

  • A(Atomicity) 原子性:原子通常指不可分割的最小粒度物质。事务中对数据的所有写操作像一个单一的操作一样执行,操作是不可分割的,要么都成功,要么都失败(终止或回滚)。

    ACID 中的原子性并不是关于多个操作的并发性,它没有描述多个线程访问相同数据会发生什么情况,这种情况是由 ACID 中的隔离性定义。原子性描述的是客户端发起一个包含多个写操作请求时可能发生的情况,比如一部分写入后,发生系统故障,包括进程崩溃,网络中断,磁盘满了等情况;这里原子性是把多个写操作作为一个原子操作,作为一个整体,万一遇到故障导致没能最终提交,事务会终止,数据库必须丢弃或撤销局部完成的更改。

  • I(Isolation) 隔离性:在处理程序时,事务保证了各种数据操作相互独立性,事务的中间状态对其它事务是不可见的。

    这里的隔离性意味着并发执行多个事务是相互独立、相互隔离的。经典数据库教材把隔离性定义为可串行化(一个一个的执行)。但是在实践中,串行化有心性能问题。比如 Oracel 11g,声称有“串行化”功能,但它本质是快照隔离,比串行化更弱的保证。

  • D(Durability) 持久性:事务应该保证所有成功提交的数据都能被持久化,即使发生故障,也不会丢失数据。

    数据库会保障一旦事务提交成功,即使硬件故障或数据库崩溃,事务所写入的数据也不会丢失。

四、MySQL中的事务

MySQL事务介绍

事务的概念其实最早是从数据库系统中来的。

事务处理系统使应用程序员能够集中精力去编写业务代码,而不必关心事务管理的各种细节。

在 MySQL 等关系型数据库中,事务是保证数据状态一致性的一个重要手段。

在 MySQL 中,一个事务可能包含多条 SQL 语句的操作,这些语句作为一个整体要么都执行成功,要么都执行失败。

MySQL 支持事务的存储引擎有 InnoDB、NDB Cluster 等,其中 InnoDB 的使用最为广泛,MyISAM 存储引擎不支持事务。MySQL 服务层并不实现事务。

MySQL 事务处理使用下面语句:

START TRANSACTION/BEGIN :开启一个事务

ROLLBACK : 回滚一个事务

COMMIT :提交事务

当然我们也可以设置 MySQL 事务自动提交模式:

  • SET AUTOCOMMIT=0 禁止自动提交
  • SET AUTOCOMMIT=1 开启自动提交

MySQL事务ACID实现

在 MySQL 中,分析下 InnoDB 存储引擎中 ACID 的实现。

  1. 隔离性

隔离性是通过不同锁机制、MVCC(多版本并发控制)来实现事务间的隔离,实现安全并发。MVCC 解决了不可重复读,或者实现了可重复读。还有的使用快照或结合快照。

MySQL 中有 4 种隔离级别,分别是 READ UNCOMMITTED(读未提交)READ COMMITTED(读已提交)REPEATABLE READ(可重复读)SERIALIZABLE(串行化)

事务隔离级别是要解决什么问题?

  1. 脏读

    脏读是指读到了其他事务未提交的数据。未提交的数据意味着数据可能会回滚,也就是最终可能不会存到数据库里,不存在的数据,这就是脏读。

  2. 可重复读

    在一个事务内,最开始读到的数据,和事务结束前任意时候读到的同一批数据是一致的。这个通常针对数据更新操作。

  3. 不可重复读

    与上面的可重复读形成对比,在同一事务内,不同的时刻读到的同一批数据可能不一样,因为这批数据可能会受到其它事务的影响。比如事务更改了这批数据并提交了。这个通常针对更新操作。

  4. 幻读

    幻读是针对数据插入操作。

    比如有 2 个事务 A 和 B,事务 A 对一批数据作了更改,但是未提交,此时事务 B 插入了与事务 A 更改前的数据记录相同的记录,并且事务 B 先于 事务 A 提交了,此时,在事务 A 中查询,会发现刚刚更改的数据未起作用,看起来没有修改过,但真实情况是,这是事务 B 插入进来的数据,这种情况感觉让用户产生了幻觉,这就是幻读。

事务隔离就是为解决上面提到的脏读、不可重复读、幻读这 3 个问题, 下表对 4 种隔离级别对解决这 3 个问题,可以和不可以:

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
串行化 不可能 不可能 不可能

上图说明:

可能 - 可能出现问题。比如 读未提交 隔离级别可能出现脏读问题。

不可能 - 表示不可能出现问题。比如 读已提交 隔离级别不可能出现脏读问题。

串行化的隔离级别最高,可以解决所有这 3 个问题 - 脏读、不可重复读、幻读。其它隔离级别只能解决部分问题,甚至有的隔离级别都不能解决。

为下文作准备:InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志)。MySQL Server 提供了 binlog日志。

  1. 原子性

undo log 保证事务的原子性。它记录了事务开始前需要回滚的一些信息。事务失败需要回滚时,可以从 undo log 日志撤销已经执行的 SQL,回滚就是一个反向操作,事务提交是正向操作 ->,回滚就是反向操作 <-。

  1. 持久性

持久性就是事务操作最终要持久化到数据库中,持久性是由 内存+redo log 来保证的,MySQL 的 InnoDB 存储引擎,在修改数据的时候,会同时在内存和 redo log 中记录这次操作,宕机的时候可以从 redo log 中恢复数据。

redo 日志记录了事务 commit 后的数据,用来恢复未写入 data file 的已成功事务更新的数据。

如果上面原子性、隔离性、持久性都实现了,那么一致性也就实现了。

一个问题:redo log 和 undo log 区别是什么?

  • undo log 记录了事务开始前的数据状态,记录的是更新之前的值
  • redo log 记录了事务完成后的数据状态,记录的是更新之后的值

事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务;事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务。

来看张图:

image-20230705173540921

上面就是对 MySQL 中 AID 的实现原理简单介绍分析。

更详细的原理理解可以看《凤凰架构》- 本地事务

五、本地事务

MySQL 中的事务也叫本地事务,有的人也译为局部事务

后面随着业务发展扩大,构建分布式系统时,会出现很多问题,为了构建容错的分布式系统,保障容错系统的技术之一就是分布式事务。下节会讨论它。

其实还有其它事务概念,本地事务还对应着全局事务。本地事务是指仅操作单一数据资源、不需要全局事务管理器进行协调的事务。

本地事务是最基础的一种事务解决方案,适用于单个服务的数据源场景。

image-20230704140901046

​ (本地事务)

分布式事务则要解决多服务的多个数据源场景。比如常见的电商微服务架构中,购物服务(shopping-service)由订单服务(order-service)和库存服务(inventory-service)组成,这里可能涉及到分布式事务:

image-20230704143020383

对于本地事务,开发语言都会对数据库提供的事务接口进行封装,然后在应用。

在 Go 语言中,对 MySQL 事务操作提供了 3 个操作函数:

// 开始事务
func (db *DB) Begin() (*Tx, error)
// 回滚事务
func (tx *Tx) Rollback() error
// 提交事务
func (tx *Tx) Commit() error

简单示例代码:

// sqlx 对事务的操作,github.com/jmoiron/sqlx
database, err := sqlx.Open()
conn, err := database.Begin()
res, err := conn.Exec()
if err != nil {
   conn.Rollback()
   return
}
res, err = conn.Exec()
conn.Commit()

在 JAVA 语言中,JDBC 对数据库事务操作进行了封装,JDBC 事务操作示例代码:

Connection conn = openConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 回滚事务:
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

六、分布式事务介绍和分布式理论

分布式事务简介

在分布式系统中,可能产生故障的地方太多了,比如服务器故障,操作系统问题,网络包丢失、数据包顺序错乱、数据包延迟,应用程序出bug,还有时间偏差等各种各样的错误出现。怎么来解决这些问题?一是直接停止服务,这显然会严重影响用户使用。二是即使某些应用服务发生了故障,整个系统依然能够对外提供服务。为此,我们要构建一个可容错的系统,在某些应用服务发生故障时也能提供服务。

比如在微服务架构中,构建一个容错的微服务系统,也叫服务治理,有哪些措施、哪些方案、哪些技术来保障系统容错性?有如下部分措施:

  • 流量控制,有限流措施
  • 服务降级
  • 服务熔断
  • 超时重试
  • 快速失败
  • API 隔离,像船舱壁一样
  • 负载均衡

当然上面的这些措施不仅在微服务中可以使用,其它应用服务也可以使用,只不过这些在微服务架构中使用得比较多。

上面说到的事务,其实也可以看作是构建容错系统一种方案,只不过事务是在应用程序与数据库之间保障数据一致性,通过事务可以有如下保障:

  • 事务出现故障可以回滚,这是原子性保障
  • 并发访问数据库没有问题,这是隔离性保障
  • 设备故障时可恢复原数据,这是持久性保障

本地事务是解决单数据源的数据一致性问题,这里的一致性其实是一个抽象概括,要保证一致性,需要原子性、隔离性、持久性共同来保障。

分布式事务解决的是多数据源的数据一致性问题。这个问题怎么解决?

一致性问题是分布式系统中的一个主要问题之一。通过 CAP 定理(Consistency 一致性、Availability 可用性、Partition Tolerance 分区容错性),也可知道分布式系统的 3 大特征。CAP 定理说明了这 3 大特征只能同时兼顾其中 2 个。

分布式基础理论

CAP定理

CAP 定理(Consistency 一致性、Availability 可用性、Partition Tolerance 分区容错性,CAP 由英文单词首字母组成)是在 2000 年 7 月由加州大学伯克利分校的 Eric Brewer 教授提出,两年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 以严谨的数学推理证明了 CAP 猜想,CAP 正式从猜想变为分布式计算领域所公认的著名定理。

这个定理描述了在一个分布式系统中,涉及共享数据时,CAP 定理中的 3 个特征最多只能同时满足其中 2 个:

image-20230704151953404

​ (来自:https://en.wikipedia.org/wiki/CAP_theorem)

  • 一致性Consistency):任何用户(客户端)同时看到的数据都是一致相同的,无论是从分布式系统的哪个节点。

    在分布式系统中,数据一般会存储到不同的节点中,当数据写入一个节点时,要将此数据复制到分布式系统中所有节点中,都需要写入成功。如果数据更新后要求所有用户必须读取到最新的相同数据,这种一致性叫强一致性(或严格一致性)。

  • 可用性Availability):可用性指任何用户请求分布式节点中的数据时都能获取响应,即使有一个或多个节点发生故障。

  • 分区容错性Partition Tolerance):分区指的是分布式系统中部分节点因为网络故障导致彼此通信中断或延迟。分区容错性指的是即使分布式系统中部分节点因网络故障导致通信中断或延迟,分布式系统也能够正常提供服务。

    发生网络分区时,系统就不能在某一时限内达成数据一致性,因为网络通信中断或延迟,数据复制会延迟,多个节点的数据在一定时限内可能不一致,这种情况不能做到强一致性,但可以做到最终一致性。下面会讲到另外一个理论 BASE,它里面就特别说到最终一致性。

CAP 定理已经有严格的数学证明,只能 3 者取其 2。在一般情况下,会选择 P,放弃 A 和 C 其中的一个。因为网络是永远不可靠的。比如在多数 NoSQL 和 分布式缓存框架都是 AP 系统。当然也有 CP 系统数据库。

在分布式系统中,通常会牺牲掉 C 一致性,但是数据又追求一致性,这不是很矛盾?怎么解决?如是人们又把一致性重新定义 - -!,把 CAP 和 ACID 中的一致性称为强一致性,把牺牲掉 C 的 AP 系统尽可能获取一致性数据的行为称为弱一致性。在弱一致性里,有一个特例,被称为最终一致性,下面 BASE 理论会讲到。

BASE理论

BASE 理论是 eBay 的系统架构师 Dan Pritchett 在 2008 年,在 ACM 发表的论文《Base: An Acid Alternative》中提出的,该论文总结了一种除了 ACID 的强一致性外,还有 BASE 理论来达成一致性目的 - 最终一致性。

BASE 理论:

  • 基本可用性(Basically Available):在分布式系统出现不可预知的故障时,允许系统部分节点失效。

  • 软状态(Soft State):有的也译作柔性事务。指允许系统中的数据出现中间状态,这个中间状态不会影响系统的整体可用性。

  • 最终一致性(Eventually Consistent):系统中的所有节点数据(包括中间状态的数据),在经过一段时间后,最终达到一个一致的状态。最终一致性是系统保证数据最终能够达到一致,而不需要实时保证数据强一致性。

BASE 理论允许数据在一段时间内是不一致性的,但最终会达到一致性状态。

七、分布式事务实现方案

X/Open XA事务模式

XA 规范是 X/Open 组织(后并入The Open Group)定义的一套分布式事务处理(DTP)标准。XA 规范描述了全局事务管理器和局部的资源管理器之间的通信接口。XA 规范定义了一组标准的接口函数,包括开始全局事务、结束全局事务、提交全局事务、回滚全局事务等。通过这些接口函数,应用程序可以实现分布式事务的提交和回滚,从而保证事务的一致性和可靠性。XA 规范里面定义了各种接口在 DTP 功能模型组件之间进行通信。

在 XA 规范中,各个功能组件使用规范的 API 进行访问。比如应用程序使用 API 接口与事务管理器进行交互,事务管理器使用 API 接口与各个资源管理器进行交互。详细情况请继续看下面说明。

XA 规范也制定了一个分布式事务处理模型(X/Open Distributed Transaction Processing Model,简称 X/Open DTP Model),该模型主要有 4 个功能组件(v2版本有4个,v1版本有3个):

  • AP(Application Program,应用程序):应用程序是实现业务功能的程序。应用程序定义了全局事务的开始和结束,它通常决定每个事务分支的提交和回滚。
  • RM(Resource Manager,资源管理器):管理定义的部分公共资源,对这些资源访问需要使用 RM 提供的服务接口。比如数据库就是常见的资源管理器。一个全局事务会关联多个 RM,每个 RM 上都会创建一个全局事务分支 Branch。
  • TM(Transaction Manager,事务管理器):管理全局事务,分配标识符给事务,监控事务进程,协调所有资源管理器完成事务以及失败恢复。应用程序通过全局事务管理器定义全局事务的开始和结束。
  • CRM(Communication Resource Manager,通信资源管理器):控制一个事务或跨多个事务的应用程序之间的通信。X/Open 提供了几种比较流行的通信模式:TxRPC、XATMI、Peer-to-Peer(P2P 对等网络)。

如果加上开放式互连事务处理,OSI TP(Open System Interconnection Transaction Process,开放式系统互联事务处理)。

那就有五个了。

image-20230705114051559

​ (各组件和 XA 及 XA+ 接口,XA and XA+ Interfaces 在 XA+ Spec v2 有定义)

image-20230705024432006

​ (分布式事务处理 (DTP) 模型 v2,各组件和接口,以及接口访问方向)

从上图可以看出有各种访问接口,共有 6 个接口,有单向也有双向接口。

比如 (1)AP-RM 接口,该接口用于应用程序(AP)对资源管理器(RMs)的访问,接口访问是单向的。还有双向接口,比如 (3)XA 接口,资源管理器(RMs)和事务管理器(TM)之间接口访问是双向的。

在业务比较简单时,单体应用就可以应付,这时事务就是一个应用程序(AP)访问一个数据库(RM),没有什么远程服务调用,只需要用一个 AP、一个 RM 和 TM 就可以实现,不需要 CRM 和 OSI TP。

在业务向前发展体量变大,业务和技术系统复杂度变高时,单体应用架构逐步向微服务架构演进,以前的大单体应用拆分成多个相对较小的应用,单体大数据库也会随着应用拆分而对应的拆分,变成多个数据库。这时候的应用程序(AP)有多个,数据库(资源管理器RM)也有多个,一个服务的调用,发生一个或多个 AP 访问一个或多个 RM,分布式事务可能产生。在分布式事务中,我们可以借助 CRM 组件及 OSI TP 组件,使得 TM 能跨多个 AP、RM 来协调全局事务。

2PC 两阶段提交

2PC 二阶段提交介绍

2PC(二阶段提交,Two-phase Commit)是指在计算机网络和数据领域内,为了使基于分布式系统下的所有节点进行事务提交时保持数据一致性而设计的一种算法。二阶段提交也被称为一种协议。

在分布式系统中,每个节点虽然可以知道自己操作是成功或失败,却无法知道其它节点的操作是成功或失败。所以当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(参与者)操作结果,并最终指示这些节点是否需要把操作结果进行提交。

二阶段提交算法基本思路:

参与者将操作结果的成功或失败情况通知协调者,再由协调者根据所有参与者的反馈情况,来决定各参与者是否要提交操作还是中止操作。

整个基本算法流程可以分为两个阶段:

  • 第一阶段 - 准备提交阶段(prepare phase)。

  • 第二阶段 - 提交执行阶段(commit phase)。

第一阶段,准备提交阶段过程:

  1. 询问:协调者向所有参与者发送消息,询问是否准备提交,然后开始等待各参与者响应消息。
  2. 执行本地事务:参与者收到协调者的询问消息后,参与者执行本地事务操作,并将 Undo 和 Redo 日志信息写入到本地。此时事务还没正式提交,预提交状态。
  3. 回复:各参与者响应协调者发起的询问:如果参与者执行本地事务等操作成功,就给协调者回复一个“同意”消息,表示我准备好了;如果事务执行失败,则回复“终止”的消息。也就是给协调者发送确认消息。

这一阶段又叫投票阶段,用投票比较形象。这里投票指的就是用消息来投票。

上面过程中有一句“此时事务还没提交”是什么意思?这需要看看一个单事务提交的全过程简图才好理解,上文第四节的图:

上图中的 Commmit 提交 在这里应该是一个预提交状态,还没有真正提交。真正提交是第二阶段才做的事情。

第二阶段,提交执行阶段过程:

又分为 2 种情况,成功或失败。

成功:当协调者获得第一阶段参与者返回的响应消息 “同意”时:

  1. 发送消息:协调者收到了参与者的回复消息 “同意”,协调者向所有参与者发送 “正式提交事务” 的消息。
  2. 正式提交事务:参与者收到消息 ”正式提交事务“ 后完成事务操作,并释放整个事务占用的资源。
  3. 回复消息:事务完成后,参与者向协调者发送回复事务 “完成” 消息,就是给协调者发送确认信息。
  4. 完成:协调者收到所有参与者回复的“完成”消息,就是确认事务完成,完成事务。

失败:当协调者获得第一阶段返回的响应消息 “终止”时:

  1. 发送消息:协调者向所有参与者发送“回滚事务”的请求。
  2. 回滚事务:参与者利用之前写入 Undo 日志信息执行回滚,并释放整个事务占用的资源。
  3. 回复消息:参与者向协调者回复事务回滚“完成”消息,就是发送确认消息。
  4. 完成:协调者收到所有参与者回复的“回滚完成”消息后,就是确认回滚完成,取消事务。

举个例子来说明下:

  • X 准备组织一场观看大片的活动,他用微信向朋友 A 和 B 发起邀请:“今天一起来看大片啊,我出电影票,来的话我马上发电影票”。

    1. 假设第一种情况 A 马上看到了信息回复 X:“好啊”。不一会 B 也回复了 X:“好啊”。

    2. 假设第二种情况 A 马上回复 X:“好啊”,B 半小时后才回复。在这半小时内 X 和 A 都必须等待 B 的回复。这就是可能产生阻塞或延迟情况。这里假设是第一种情况,几乎都马上回复了 X:好啊。

    3. 假设第三种情况 A 马上回复 X:“好啊”,B 回复了 X:”没时间去啊“。

      (上面就是二阶段提交的第一阶段)

  • 如果是上面第一种情况,X 收到了 A 和 B 的肯定回复,X 于是给 A 和 B 每人发了一张电影票的二维码,他们就出发和 X 汇合。如果是上面第三种情况,X 会再给 A 发消息:“B 不去,那取消吧”,A 回复:“好吧”。

    如果是上面第二种情况,那么就得一直等待 B 的回复,在执行上面的 2 种情况步骤。

二阶段提交过程简图(成功的情况):

(在 XA 事务模式,协调者叫TM事务管理器,参与者叫RM资源管理器。)

image-20230706215000083

2PC 缺点

  • 同步阻塞,在执行过程中,参与节点的事务都是阻塞的。所有节点都等待其它节点的响应,无法进行其它操作。

  • 性能问题,因为不论是第一阶段还是第二阶段,所有参与者和协调者的公共资源都是被锁住的,只有当所有节点准备完毕,协调者才会通知进行全局事务提交,参与者进行本地事务提交后才会释放资源。这是一个比较长的过程。

  • 单点问题,协调者在 2PC 中有着很重要作用,如果协调者宕机了一直没恢复,所有参与者会受到影响,会一直等待。这里面还有一种情况会导致数据一致性问题,第二阶段中当协调者向所有参与者发送“正式提交”消息之后,发生了局部网络故障或协调者未发送完全部的“正式提交”消息突然宕机了,这时会导致只有部分参与者收到消息,消息投票协调一致出现了问题。

  • 容错性,还有一个,就是任何一个节点失败,整个事务就会失败,没有容错性。

为了解决上面提到的部分问题,具体就是协调者的单点问题和准备阶段的性能问题,后面又发展出了 3PC(Three-phase Commit,三阶段提交)协议。

3PC 三阶段提交

3PC 三阶段提交介绍

3PC(Three-phase Commit,三阶段提交)与 2PC 二阶段提交相比有两个改动点:

  1. 三阶段提交把原本 2PC 的第一阶段“准备阶段”又细分为两个阶段 - CanCommit 和 PreCommit,把第二阶段提交执行阶段称为 DoCommit 阶段。第一阶段做的事情比较多,回滚复杂,在拆分简化下。

    CanCommit、PreCommit 和 DoCommit,这就是三阶段提交协议的三个阶段。

  2. 协调者和参与者同时引入超时机制。

image-20230707135118489

第一阶段:CanCommit 询问阶段

这一阶段就是协调者和参与者简单的消息交互,本身再没有做多余的复杂操作。不像 2PC 第一阶段除了消息交互还要做 Undo/Redo 等操作。

  • 询问:协调者向所有参与者发送消息询问:“你们是否能够比较顺利完成事务?”
  • 回复:各参与者如果认为自己能够顺利执行事务,回复消息:”是/Yes“;不能顺利执行,回复:“否/No”。就是向协调者发送确认消息。

第二阶段:PreCommit 预提交阶段

协调者获得 CanCommit 阶段中参与者回复的消息后,会执行下面 2 个不同操作步骤:事务预提交或中断事务。

操作步骤1:事务预提交

  1. 发送预提交消息:收到 CanCommit 阶段参与者“是”的确认消息后,协调者向所有参与者发送事务预提交的消息,并进入 prepared 状态。

  2. 事务预提交:参与者收到协调者预提交的消息后,参与者执行事务操作,记录 Undo、Redo 日志信息。事务进入预提交状态。

  3. 回复消息:参与者预提交状态执行情况,执行成功,向协调者回复消息:“是”;执行失败回复:“否”。都需要向协调者发送确认消息。

操作步骤2:中断事务

  1. 发送中断消息:收到 CanCommit 阶段参与者“否”的确认消息后,协调者向所有参与者发送中断事务的消息。
  2. 中断事务:参与者收到中断事务的消息后,中断事务操作。

第三阶段:DoCommit 正式提交阶段

这个阶段也有 2 种不同操作步骤,在上面 PreCommit 操作步骤1 中,事务预提交状态执行情况回复有 2 种。

操作步骤1:正式提交事务,PreCommit 预提交状态回复:“是”

  1. 发送正式提交消息:协调者收到了参与者事务预提交状态确认消息 “是” 后,协调者向参与者发送“正式提交”事务的消息
  2. 正式提交事务:参与者收到协调者发送的“正式提交”的消息后,从预提交状态变成正式提交事务,事务提交执行完后释放事务执行期间占用的资源。
  3. 回复消息:参与者事务彻底完成后,向协调者发送“完成”的确认消息。
  4. 完成:协调者收到参与者“完成”事务的消息后,确认完成事务

上面这一步骤过程与 2PC 的第二阶段步骤过程几乎相同。

操作步骤2:中断事务,PreCommit 预提交状态回复:“否”

  1. 发送中断消息:协调者收到了预提交状态确认消息 “否” 后,协调者向参与者发送“中断”事务执行的消息。
  2. 回滚事务:参与者收到中断事务的消息之后,回滚事务,释放事务占用的资源
  3. 回复消息:回滚事务完成后,向协调者发送确认消息。
  4. 完成:协调者收到所有参与者的确认消息后,中断事务完成。

3PC 优缺点

这里指的是相对于 2PC 的优缺点。

优点:

  • 1.协调者和参与者都引入了超时机制,优化了在 2PC 中长时间收不到消息导致长时间阻塞的情况,一定时间内收不到消息就超时处理。
  • 2.优化了阻塞范围,3PC 把 2PC 第一阶段细分成 2 个阶段,这样 3PC 第一阶段就是很简单的操作,这个阶段不会阻塞。
  • 3.3PC 的 PreCommit 中中断事务回滚的操作是一个轻量级操作,这个也是因为细分为 2 个阶段缘故。
  • 4.由于增加了 CanCommit 询问阶段,事务成功提交的把握增大。即使不成功,这个阶段回滚风险也变小。同上 3。
  • 5.协调者单点风险减小,在 3PC 中,如果 PreCommit 阶段之后发生了协调者宕机,下一阶段 DoCommit 无法执行了,但是 3PC 这时默认操作是提交事务而不是回滚事务或持续等待,就相当于避免了协调者单点问题。为什么能这样?因为 PreCommit 阶段的存在。

3PC 对单点问题、阻塞问题和回滚时性能都有所改善。

缺点:

  • 数据不一致没有改善,比如进入 PreCommit 之后,协调者发出的事务预提交状态的消息是:”否“ - 失败的,刚好此时网络故障,有部分参与者直到超时都未能收到“否”的消息,这些参与者将会错误提交事务,导致参与者之间数据不一致的问题。

SAGA 事务模式

saga 事务介绍

saga 事务介绍:

saga 事务是 Hector Garcaa-Molrna & Kenneth Salem 在 1987 年发表的论文 SAGAS 中提出的,内容是关于怎么处理 long lived transactions(长事务)的方法,它的核心思想:

将大事务/长事务拆分为多个短事物,这些事务由 saga 事务协调器协调,如果每个短事务都成功提交完成,那么全局事务就成功完成;如果某个步骤失败,那么根据相反顺序调用补偿操作或进行重试操作。

每个 saga 事务由一系列的 sub-transaction Ti(i 表示数字)组成,每个 Ti 对应一个补偿操作 Ci,补偿操作用于撤销 Ti 执行操作。

image-20230707174959406

saga 事务恢复策略有2种:

重试和回滚补偿

  1. forward recovery 向前恢复

    重试失败的事务,假设每个短事务最终都会执行成功。

    比如事务执行顺序是 T1、T2、T3,恢复顺序也是按照这个顺序执行,策略就是如果 T1 执行失败,那么重试 T1;如果 T2 执行失败,那么重试 T2,以此类推,哪个短事务执行失败就重试哪个事务。

  2. backward recovery 向后恢复

    任一事务失败,补偿所有已完成的事务。

    比如事务执行顺序是 T1、T2、T3,如果 T2 事务执行失败了,那么就从 C2 开始执行补偿事务,然后在执行 C1,回滚整个 saga 事务执行结果。

saga 分布式事务协调有2种:编排和控制

  1. 编排 Choreography

    参与者(子事务)之间的调用、决策和排序,通过交换事件来进行,是一种去中心化的模式。参与者之间通过消息进行通信,通过监听器监听其它参与者发出的消息,然后执行后续业务逻辑。

  2. 控制 Orchestration

    saga 提供一个控制类,它帮助协调参与者之间的工作。事务执行命令由控制类发起,按照逻辑顺序请求 saga 的参与者,从参与者哪里接收反馈消息后,控制类在向其它参与者发起调用。这个模式有点像 2PC 和 3PC。

适用场景:

  • 业务流程长、业务流程多

saga 优缺点

saga 是一种弱一致性协议,整个事务执行过程中本地事务可以处于不一致性状态,但是最终数据会达到一致。

优点:

  • 一阶段提交的是本地事务,无锁,高性能
  • 事件驱动架构,异步执行,高吞吐
  • 简单,补偿服务易于实现

缺点:

  • 隔离性差,不保证隔离性
  • 补偿事务有

TCC 事务

TCC 事务介绍

TCC 是 Try,Confirm,Cancel 是哪个单词的首字母组成,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

它是一种分布式事务解决方案,可用于跨数据库、跨业务操作的数据一致性问题。

在上面文章里,XA 的 2PC 两阶段提交中资源管理器(RM)提供了 3 个操作:准备、提交、回滚。事务管理器(TM)分 2 个阶段协调所有资源管理器:第一阶段询问资源管理器“准备”好了没,如果所有资源准备成功了,则第二阶段执行所有事务“提交”操作并释放资源;否则在第二阶段执行事务的“回滚”操作并释放资源。

这里的资源管理器有多种实现方式,TCC 就是基于业务逻辑实现的资源管理器,TCC 的 Try、Confirm、Cancel 3 个方法均由业务编码实现。TCC 是一种业务侵入比较强的事务解决方案,业务处理过程分为 “尝试执行业务” 和 “确认/取消执行业务” 两个子过程。如 TCC 名称,它有 3 个操作步骤:

  • Try:尝试执行业务,完成所有业务可执行性的检查(保障一致性),并且预留好全部需要用到的资源(保持隔离性)。

  • Confirm:确认执行业务,不进行业务检查,直接使用 Try 步骤准备的资源来完成业务处理。Confirm 步骤可能会重复执行,因此这个步骤需要幂等性。

  • Cancel:取消执行业务,释放 Try 步骤预留的业务资源。Cancel 可能会重复执行,因此也需要幂等性。

把上面步骤在分阶段,可以把 TCC 看作是应用层实现的 2PC 两阶段提交:

  • 第一阶段:Try 操作,确认资源是否可执行,同时对要用到的资源进行锁定,

  • 第二阶段:Confirm 操作 或 Cancle 操作。如果第一阶段 Try 执行成功,那么就开始真正执行业务,并释放资源;如果第一阶段 Try 执行失败,那么执行回滚操作,预留的资源取消,使资源回到初始状态。

TCC 分布式事务的角色,有 3 个,与前面讲到的 XA 事务角色优点类似:

  • AP 应用程序:发起全局事务,定义全局事务包含哪些

  • RM 资源管理器:负责分支事务各资源管理

  • TM 事务管理器:负责协调全局事务执行,包括 Confirm,Cancel 的执行,并处理网络异常

一个成功执行 TCC 事务的时序图:

image-20230708030513160

​ (来源:https://www.dtm.pub/practice/tcc.html#tcc组成 DTM 事务框架)

image-20230708031335071

​ (事务框架 seata 中 tcc 模型)

TCC 优缺点

优点:

  • TCC 隔离性好,适合用于需要强隔离的分布式事务
  • TCC 保证数据最终一致性,不是强一致性
  • 应用层实现,灵活性高。这是优点也是缺点。
  • 性能比较高

缺点:

  • 业务侵入性比较强
  • 开发成本高,需要动手开发代码
  • 不适合强一致性场景

本地消息表方案

本地消息表介绍

本地消息表方案是可靠消息最终一致性的一种。

最终一致性这个概念是由 ebay 架构师 Dan Pritchett 在 2008 年发表在 ACM 的论文 Base: An Acid Alternative。该论文总结了除了 ACID 的强一致性之外,还可以使用 BASE 来达成最终一致性。这些上面都有讲。

什么是可靠消息呢?

可靠消息指的是事务发起方(消息发送者)完成本地事务后,发送消息,事务参与方(消息接收者)一定能够接收到消息然后成功处理事务。

上面可靠消息如果不加一个存储“消息”的中间件,那么跟 2PC 的操作步骤很相似,变成了一个同步的操作情况,消息发送后接收者直接接收,不存储消息。如果加一个存储消息的中间件,那么就变成了异步读写方案了,解耦了消息发送方和消息接收方,天然就不是一个强一致性方案。

这个存储消息的地方,可以是本地消息表(MySQL、Redis等)、消息队列等各种存储中间件。这里介绍的是本地消息表方案。

本地消息表实现事务的过程简析:

image-20230710005321541

  • 1.在事务发起方,新建一个存储事务消息表即是本地消息表。
  • 2.事务发起方在一个事务中处理业务和把消息状态写入消息表,并发送消息到消息中间件。发送消息到消息中间件失败会重试发送。
  • 3.事务参与方,从中间件中读取消息,然后在一个本地事务中完成自己的业务逻辑。如果本地事务处理成功,就返回一个处理结果消息:成功。如果本地事务处理失败,给事务发起方发送一个业务补偿的消息,通知事务发起方进行回滚操作。
  • 4.事务发起方读取中间件的处理结果,如果是成功的消息,那么更新消息表状态。如果是失败的消息,那么进行事务回滚操作。
  • 5.定时扫描还没处理的消息或失败消息,在发送一遍,进行事务处理。

以上是最简单的流程,好多地方失败的流程没有写出来。

本地消息表优缺点

优点:

  • 消息可靠性由应用开发保证,也就是说自己可以灵活实现
  • 运行效率有提高

缺点:

  • 与业务逻辑绑定
  • 需要设计消息表,还需要定时后台任务,自己实现复杂业务逻辑

还有基于MQ事务消息的方案,也是最终一致性方案的一种。

八、参考

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/jiujuan/p/17542160.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!