第 3 章 事务处理

  • 原子性(Atomic):在同一项业务处理过程中,事务保证了对多个数据的修改,要么同时成功,要么同时被撤销。
  • 隔离性(Isolation):在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。
  • 持久性(Durability):带图就当保证所有成功被数据修改才能够正确地被持久化,而不丢失。
  • 一致性(Consistency):系统中所有的数据都是符合期望的,且相互关联的数据之间不会产生矛盾。

A、I、D 是手段,C 是目的。

3.1 本地事务

本地事务(Local Transaction),指仅操作单一事务资源的、不需要全局事务管理器进行协调的事务。

3.1.1 实现原子性和持久性

Commit Loggin

  • 数据必须写入磁盘等持久化存储器后才能拥有持久性,而"写入磁盘"这一操作即不是一定成功的(应用程序崩溃),且原子的。
  • 当以下情形发生时,原子性和持久性将无法得到保证
    • 未提交事务,部分写入时崩溃。此时需要重启后恢复已写入的数据,以保证原子性;
    • 已提交事务,还未写入时崩溃。此时需要重启后重新写入已提交的数据,以保证持久性;
  • 将修改数据的全部修复,包括修改什么数据、存在于哪个内存页和磁盘块,旧值新值等,以日志的形式追加记录到磁盘中。
  • 数据库在日志中看到代表事务成功的"提交记录(Commit Record)"时,才会根据日志,将数据写入磁盘。
  • 写入完成后,追加一条"结束记录(End Record)"表示事务已完成持久化,这种事务实现方式被称为"Commit Logging(提交日志)"

Write-Ahead Logging

  • Commit Loggin 的缺陷在于,所有对数据的真实修改都必须发生在事务提交以后------即 Commit Record 以后,在此之前即便磁盘 I/O 有足够空闲,即便某个事务修改的数据量非常庞大,占用了大量的内存缓冲区,也无法提前修改数据。这无疑对数据库的性能提升非常不利。于是就有了"Write-Ahead Logging(提前写入日志)"
  • Write-Ahead Logging 将何时写入变动数据,按照事务提交为界,划分为 FORCE 和 STEAL 两类情况
    • FORCE:当事务提交后,要求数据必须同时完成写入称为 FORCE,否则为 NO-FORCE。绝大多数据库采用的都是 NO-FORCE 策略。由于日志的存在,数据可能随时持久化。
    • STEAL:在事务提交前,允许数据提前写入称为 STEAL,否则为 NO-STEAL。从优化磁盘 I/O 性能考虑,STEAL 更好。
  • Commit Logging 允许 NO-FORCE,但不允许 STEAL。
  • Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL。它使用 Undo Log------当变动数据写入磁盘时,先记录 Undo Log,注明修改数据的位置,新旧值等,以便在事务回溯或崩溃恢复时进行擦除。
  • Undo Log 被称为"回滚日志",此前记录的用于崩溃恢复时重演数据变动被称为 Redo Log(重做日志)。
  • Write-Ahead Logging 在崩溃恢复时会执行以下三个阶段的操作
    • 分析阶段(Analysis):从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合,至少包括 Transaction Table 和 Dirty Page Table 两个部分。
    • 重做阶段(Redo):重演待恢复的事务集合------找出所有包含 Commit Record 的日志,将这些日志写入磁盘后增加一条 End Record,再移出待恢复事务集合
    • 回滚阶段(Undo):经过重做阶段剩余的都是需要回滚的事务,根据 Undo Log 将它们提前写入的信息擦除。

FORCE和STEAL的四种组合关系

TIPS:Shadow Paging(影子分页)

修改数据时,并不直接修改原先的数据,而是先将数据复制一份副本(Shadow),保留原数据,修改副本数据。当事务成功提交,所有修改都持久化后,修改数据的引用指针为副本数据。

Shadow Paging 实现事务要比 Commit Logging 更加简单,但涉及隔离性与并发锁时,并发能力相对有限,所以在高性能数据库中并不多见。
关于 Redo Log 和 Undo Log 的理解

  1. 基础事务的流程:step1:内存中修改完成 -> step2:commit -> step3:写入磁盘。由于被修改数据是统一在 commit 之后写入,所以当 step3 发生崩溃时,只需要通过 Redo Log 重做,将被修改数据写入磁盘即可。这也是 Commit Logging 的原理。
  2. 但是要求被修改数据是统一在 commit 之后写入会降低数据库性能(见 Write-Ahead Logging 第 1 小点),所以需要在 commit 之前,就"提前"将被修改数据写入磁盘。这就导致了另一个问题------如果事务没有 commit 而是 rollback 了,或者 commit 失败了,如何"擦除"已写入的数据。这就引入了 Undo Log,通过它回滚掉 commit 之前已经写入磁盘的被修改数据。这就是 Write-Ahead Logging 的原理。
  3. 所以 Redo Log 记录所有被修改记录,用于崩溃时重演数据变动,而 Undo Log 只记录 Commit 前提前写入的被修改数据,用于事务回滚或者崩溃时擦除变动。

3.1.2 实现隔离性

要在并发下串行访问数据,现代数据库提供了以下三种锁来实现

  • 写锁(Write Lock,或排他锁,eXclusive Lock,X-Lock):如果数据被加了写锁,那么只有持有写锁的事务才能对数据进行写入操作;其他事务不能加读写锁。

  • 读锁(Read Lock,或共享锁,Shared Lock,S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再加写锁。如果只有一个事务加了读锁,允许其将锁升级为写锁。

  • 范围锁(Range Lock):对于某个范围的数据加排他锁。不仅无法修改该范围内已存在的数据,也不能在该范围内新增或删除数据。

    sql 复制代码
    SELECT * FROM books WHERE price < 100 FOR UPDATE;

四种隔离级别

可串行化(Serializable):对事务所有读、写的数据全都加上读锁、写锁和范围锁。隔离程序最高但并发访问的吞吐量最低。

可重复读(Repeatable Read):对事务涉及的数据加读锁和写锁,直到事务结束,但不加范围锁。存在幻读(Phantom Reads)问题------在事务执行的过程中,两个完全相同的范围查询得到了不同的结果。原因是没有范围锁来禁止在该范围内插入新的数据。

读已提交(Read Committed):对事务涉及的数据加的写锁会一直持续到事务结束,但读锁会在查询操作完成后立即释放。存在不可重复读(Non-Repeatable Reads)问题------在事务执行过程中,对同一行数据的两次查询得到不同的结果。原因没有贯穿整个事务周期的读锁,无法禁止读取过的数据发生变化。

读未提交(Read Uncommitted):对事务涉及的数据只加写锁,一直持续到事务结束,但完全不加读锁。存在脏读(Dirty Reads)问题------事务在执行过程中,一个事务读取到了另一个事务未提交的数据。原因是在数据上完全不加读锁,反而令它能读到其他事务加了写锁的数据。

由此可见,不同的隔离级别,并不是数据库的某种固有属性或设定,而是各种锁在不同加锁时间上组合应用所产生的结果,以锁为手段来实现隔离性,才是数据库表现不同隔离级别的根本原因。

MVCC

MVCC(多版本并发控制,Multi-Version Concurrency Control),一种针对"一个事务读,另一个事务写"的隔离问题而出现的无锁(特指读取时不需要加锁)优化方案。

  • MVCC 对数据库的任何修改都不会直接覆盖原数据,而是产生一个"新版"副本与"老版"数据共存,以此达到读取时完全不加锁的目的。这里的"版本"可以理解为每一行记录的两个看不见的字段:CREATE_VERSION 和 DELETE_VERSION,记录了事务 ID,而事务 ID 是一个全局严格递增的值。
  • 写入数据时,遵守以下规则
    • 插入数据时:CREATE_VERSION 记录插入数据的事务 ID,DELETE_VERSION 为空
    • 删除数据时:DELETE_VERSION 记录删除数据的事务 ID,CREATE_VERSION 为空
    • 修改数据时:将修改数据视为"删除旧数据,插入新数据",即原数据 DELETE_VERSION 记录修改数据的事务 ID,CREATE_VERSION 为空;新数据 CREATE_VERSION 记录修改数据的事务 ID,DELETE_VERSION 为空
  • 读取数据时,遵守以下规则
    • 隔离级别是可重复读时:总是取 CREATE_VERSION <= 当前事务 ID 的记录,若数据有个版本则取事务 ID 最大的那条;
    • 隔离级别是读已提交时:总是取最新版本即可,即最近被 Commit 的那个版本的数据记录。
    • 隔离级别是读未提交和可串行化时:不必使用 MVCC,因为读未提交直接在原记录修改即可;而可串行就是要阻塞其他事务读取,与 MVCC 相悖。

3.2 全局事务

全局事务(Global Transaction),这里被限定为一种适用于单个服务使用多个数据源的场景的事务解决方案。

JTA(JSR 907 Java Transaction API),基于 XA(eXtended Architecture)模式在 Java 语言中实现的全局事务处理标准,主要有两个接口:

  1. 事务管理器的接口:javax.transaction.TransactionManager,给 Java EE 服务器提供容器事务(由容器自动负责事务管理)使用的,还提供了另外一套 javax.transaction.UserTransaction 接口,用于通过程序代码手动开启,提交和回滚事务。
  2. 满足 XA 规范的资源定义接口:javax.transaction.xa.XAResource,任何资源(JDBC、JMS等)如果想到支持 JTA,只要实现 XAResource 接口中的方法即可。

以"买书"为例:

java 复制代码
public void buyBook(PaymentBill bill) {
    userTransaction.begin();
    warehouseTransaction.begin();
    businessTransaction.begin();
	try {
        userAccountService.pay(bill.getMoney());
        warehouseService.deliver(bill.getItems());
        businessAccountService.receipt(bill.getMoney());
        userTransaction.commit();
        warehouseTransaction.commit();
        businessTransaction.commit();
	} catch(Exception e) {
        userTransaction.rollback();
        warehouseTransaction.rollback();
        businessTransaction.rollback();
	}
}

businessTransaction.commit();执行失败时,businessTransaction.rollback();可以起效,但在它之前的userTransaction.rollback(); warehouseTransaction.rollback();由于已经被提交,再回滚也无济于事了。

两段式提交(2 Phase Commit,2PC)

  • 准备阶段:又叫投票阶段。这一阶段,协调者询问事务的所有参与者是否准备好提交,是则回复 Prepared,否则回复 Non-Prepared。准备操作是在重做日志(redo log)中记录全部内容,只是不写入最后一条 Commit Record,且仍持有锁,维持数据对其他非事务内观察者的隔离状态。
  • 提交阶段:又叫作执行阶段,协调者如果在上一个阶段收到所有事务参与才回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,之后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作。否则,任意一个参与者回复了 Non-Prepared 消息,或未回复,协调者先持久化本地事务为 Abort,再向所有参与者发送 Abort 指令,所有参与者立即执行回滚操作。

两段式提交能够保证一致性的前提条件:

  1. 必须假设网络在提交阶段的短时间内是可靠的,保证提交阶段不会丢失消息。同时也假设网络通信在全过程都不会出现误差,即可以丢失消息,但不会传递错误的消息。
  2. 必须假设因为网络分区、机器崩溃或者其他原因崦导致失联的节点最终能够恢复,不会永久处于失联状态。由于在准备阶段已经写入了完整的重做日志,所以当失联机器一旦恢复,就能从日志中找出已准备妥当但并未提交的事务数据,并向协调者查询该事务状态,确定提交还是回滚。

3.2-1 两段式提交的交互时序示意图

两段式提交原理简单,不难实现,但有几个显著缺点:

  1. 单点问题:协调者作用举足轻重,参与者可以超时、宕机、但协调者一旦宕机不能恢复,或没有正常发送指令,那所有参与者都将受到影响。
  2. 性能问题:所有参与者被绑定为一个整体,期间经过两次远程服务调用,三次数据持久化(准备阶段写重写日志,协调者状态持久化,提交阶段写 Commit Record),整个过程受木桶效应影响。
  3. 一致性风险:提交阶段,协调者已经持久化本地事务状态,这时候网络断开,无法向参与者发送 Commit 指令,就会导致部分数据(协调者的)已提交,但部分数据(参与者)的未提交(也没回滚),产生了数据不一致的问题。

三段式提交(3 Phase Commit,3PC),解决两段式提交的性能问题和单点问题。

  • 把两段式提交的准备阶段分为 CanCommit 和 PreCommit 两个阶段,提提交阶段改称为 DoCommit。
  • CanCommit 阶段是一个询问阶段,协调者让每个参与者评估事务是否有可能顺利完成。以此来增加事务能够成功提交的概率,降低作无用功的风险。部分解决了性能问题(仅在事务的回滚场景有提升,在事务正常提交的场景,因为多了一次询问,甚至有所下降)
  • PreCommit 阶段之后如果发生了协调者宕机,参与者没有等到 DoCommit 消息的话,会默认将事务提交,以此避免协调者单点问题的风险。但却增加了一致性风险,尤其是当协调者宕机时是想发出 Abort 而不是 Ack 指令时,参与者会错误地提交事务。

3.2-2 三段式提交的交互时序示意图

3.3 共享事务

共享事务(Share Transaction)是指多个服务共用同一个数据源。

3.4 分布式事务

分布式事务(Distributed Transaction)特指多个同务同时访问多个数据源的事务处理机制。

CAP与ACID

CAP(Consistency、Availability、Partition Tolerance)描述了一个分布式的系统中,涉及共享数据问题时,以下三个特性最多只能同时满足其中两个:

  • 一致性(Consistency):代表数据在任何时刻、任何分布式节点中所看到的都是符合预期的。
  • 可用性(Availability):代表系统不间断地提供服务的能力。
  • 分区容忍性(Partition Tolerance):代表分布式环境中部分节点因网络原因而彼此失联后,即与其他节点形成"网络分区"时,系统仍能正确地提供服务的能力。

以下为具体场景示例说明 CAP

3.4-1 Fenix's Bookstore的服务拓扑示意图

假设某次交易分别由"账号节点1"、"商家节点2"和"仓库节点N"联合进行响应。当用户购买一件100无的商品后,账户节点1首先应给该用户账户扣减100元,再通知本集群的其他节点同样扣减100元,此时将面临以下可能的情况:

  • 没有及时同步给其他节点,那么一次交易由其他节点处理时,将会因为余额不正确产生一致性问题
  • 在同步过程中所有服务必须暂停提供服务,直接同步完成,产生可用性问题
  • 网络问题导致一部分节点无法同步,此时提供的服务可能是不正确的,集群能否容忍部分节点失联而继续正常提供服务,就是分区容忍性

以上只是账号服务集群自身的 CAP 问题,在账户服务集群、商家服务集群和仓库服务集群之间,也存在 CAP 问题。

  • 如果放弃分区容忍性(CA without P),意味着我们将假设节点之间通信永远是可靠的。但永远可靠的通信在分布式系统中必定不成立,只要用到网络来共享数据,分区现象就会始终存在。
  • 如果放弃可用性(CP without A),意味着我们将假设一旦网络发生分区,节点之间的信息同步可以无限制地延长,此时相当于一个系统使用多个数据源的"全局事务"问题,我们可以通过 2PC/3PC 等手段,同时获得分区容忍性和一致性。
  • 如果放弃一致性(AP without C),意味着我们将假设一旦发生分区,节点之间所数据可能不一致。这是目前分布式系统的主流,因为 P 是分布式网络的天然属性,无法丢弃;而 A 是建设分布式的目的,除了某些涉及金钱交易的服务,多数系统是不能容忍节点越多可用性反而越低的。

使用 AP 虽然放弃了一致性,但它需要保证"最终一致性(Eventual Consistency)",即如果数据在一段时间内没有被另外的操作所更改,那它最终将会达到与强一致性过程相同的结果。

ACID 被称为"刚性事务",以下几种分布式事务的常见做法则被称为"柔性事务"

可靠事件队列

3.4-2 可靠事件队列时序图

这种靠着持续重试来保证可靠性的解决方案在计算机其他领域有专门的名字:最大努力交付(Best-Effort Delivery),譬如 TCP 协议中未收到 ACK 应答自动重新发包。而可靠事件队列还有一种更普通的形式,被称为"最大努力一次提交(Best-Effort 1PC)",指的是将最有可能出错的业务以本地事务的方式完成后,采用不断重试的方式来促使其他关联业务完成。

TCC事务

可靠事件队列实现足够简单,但完全没有隔离性可言(本例中可能造成超售)。而 TCC 天生就适合于需要强隔离性的分布式事务中。TCC 分为三个阶段:

  • Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需要用到的业务资源(保障隔离性)
  • Confirm:确认执行阶段,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。可能会重复执行,因此需要具备幂等性
  • Cancel:取消执行阶段,释放 Try 阶段预留的业务资源,可能会重复执行,可能会重复执行,因此需要具备幂等性

3.4-3 TCC时序图

TCC 在业务执行时只操作预留资源,几乎不会涉及锁和资源竞争,具有很高的性能潜力。但它带来了更高的开发成本和业务侵入性,所以通常并不会完全靠裸编码来实现 TCC,基于基于某些分布式事务中间件(如阿里 Seata TCC 事务模式)去完成。

SAGA事务

TCC 通过预留资源的方式保证了隔离性,但某些情况下不能随意预留资源,而 SAGA 则可以避免这个问题。它由两部分组成:

  • 大事务拆分若干个小事务,将整个分布式事务 T 分解为 n 个子事务,分别为 下标 :T1 ,T2 ,......,Ti ,......,Tn 。每个子事务都应该是或者能被视为是原子行为。如果分布式事务能够正常提交,其对数据的影响(最终一致性)应与连接按顺序成功提交 Ti 等价。
  • 为每一个子事务设计对应的补偿动作,命名为 C1 ,C2 ,......,Ci ,......,C1 。Ti 与 Ci 必须满足以下条件:
    • Ti 与 Ci 都具备幂等性
    • Ti 与 Ci 满足交换交换律(Commutative),即先执行 Ti 还是先执行 Ci 效果都是一样的
    • Ci 必须能成功提交,即不考虑 Ci 本身提交失败被回滚的情形,如出现就必须持续重试直到成功,或者要人工介入。

如果 T1 到 Tn 均成功提交,那么事务顺利完成,否则,采取以下两种恢复策略之一:

  • 正向恢复(Forward Recovery):如果 Ti 事务提交失败,则一直对 Ti 进行重试,直到成功为止(最大努力交付)。T1 ,T2 ,......,Ti (失败),Ti (重试)......,Ti+1 ,......,Tn
  • 反向恢复(Backward Recovery):如果 Ti 事务提交失败,则一直执行 Ci 对 Ti 进行补偿,直到成功为止(最大努力交付)。T1 ,T2 ,......,Ti (失败),Ci (重试)......,Ci-1 ,......,C2 ,C1

与 TCC 相比,SAGA 不需要为资源设计冻结状态和撤销的操作,补偿操作往往要比冻结操作容易实现得多。

SAGA 必须保证所有子事务都得以提交或者补偿,但 SAGA 系统本身也会崩溃,所以它必须设计成与数据库类似的日志机制(SAGA Log)以保证系统恢复后重启。

SAGA 事务通常不会直接靠裸编码来实现,一般也是基于某些分布式事务中间件(如阿里 Seata SAGA 事务模式)去完成。

以数据补偿代替回滚的案例

阿里的 GTS(Global Transaction Service,Seata 由 GTS 开源而来)所提出的"AT 事务模式 ",整体上是参照了 XA 两段式提交实现的,但为了解决木桶效应,在业务数据提交时自动拦截所有 SQL,将 SQL 对数据修改前、修改后的结果分别保存快照,生成行锁,通过本地事务一起提交到操作的数据源中,相当于自动记录了重做和回滚日志。如果分布式事务提交成功,直接清理日志数据即可;如果需要回滚,就根据日志数据自动产生用于补偿的"逆向SQL"。基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。这种异步提交的模式,相比起 2PC 极大地提升了系统的吞吐量水平。而代价就是大幅度地牺牲了隔离性,甚至直接影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚并不一定是总能成功的。

相关推荐
uhakadotcom10 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
数据智能老司机16 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机16 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
c无序16 小时前
【Docker-7】Docker是什么+Docker版本+Docker架构+Docker生态
docker·容器·架构
数据智能老司机17 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
矿渣渣17 小时前
RM Cortex-A7 架构中“SEV”汇编指令解析
汇编·架构
uhakadotcom18 小时前
Flutter入门指南:快速构建高性能移动应用
面试·架构·github
月阳羊18 小时前
【无人机】无人机PX4飞控系统高级软件架构
嵌入式硬件·架构·系统架构·无人机
uhakadotcom18 小时前
MVC 和 MVVM 架构模式:基础知识与实践
后端·面试·架构