0%

三、事务处理

[toc]

事务处理#

事务有四个经典的特性ACID:

  • 原子性 (Atomicity):事务中的所有操作都必须是原子的,即不可分割或撤销的。在一个事务执行期间,所有的操作都必须同时成功或同时失败,不存在中间状态。
  • 一致性 (Consistency):事务执行的结果必须保证数据库的一致性,即数据库中的数据必须在事务开始和结束时保持一致。
  • 隔离性 (Isolation):事务之间的操作相互隔离,即一个事务的操作不会受到其他事务的影响。
  • 可用性 (Availability):事务执行期间数据库必须保持可用,即可以在任何时候进行访问和修改。

这四个特性ACID中, C其实是目的, AID是手段。
只靠内部(单数据源)可以用AID实现C
但是外部(多数据源)的情况下没法用AID保证C。

1 本地事务#

本地事务是一种最基础的事务解决方案,适用单个服务使用单个数据源的场景。
(注意,对于MyISAM来说,代码层面调用的rollback其实是空操作,引擎内置了事务处理,不需要代码调用rollback)

本地事务的实现原理来自ARIES(基于语义的恢复与隔离算法)

1.1 本地事务如何实现原子性和持久性#

本地事务中, 写入磁盘的过程可能不是原子的,是会崩溃的。
因此要考虑2个异常情况:

  • 未提交事务(调用事务的应用层代码未返回成功),数据还没改完,写了一半崩溃了,导致数据不一致,非原子性
  • 已提交事务(调用事务的应用层代码已经反悔了),但是实际磁盘内容还没写就崩了,导致数据完全没变化,非持久性。

解决方式:
引入commit log, 即将事务对数据的修改先写入commit log,写入成功代表事务成功,写入完成后再写磁盘,如果中途崩溃了就重新写入
等同于熟知的redo-log!
这也是为什么redo-log中是针对某个物理块的修改,目的就是能正确重新,不用考虑我写到哪个位置了,直接全部重刷即可。

但是这样性能太慢,希望能在事务提交完成前提前写入磁盘,但是提前写的话可能会非原子。
这时候就可以引入 undolog, 即触发回滚时,可以讲已操作的数据进行undo回滚操作。
这也是为什么undo-log记录的是一条条不可重复执行的语句。

文中还提了2个特征:
NO-FORCE:事务提交后,不强求立刻全部写入磁盘,可以延迟(commit-log,有这的存在就不着急了)
STEAL:事务提交前,可以先写入一部分数据(undo-log)

1.2 本地事务如何实现隔离性#

隔离性主要就是依赖 数据库锁和数据库隔离级别实现。
书中用作者自己的话简述了一遍从 可串行化 到 可重复读 到 读已提交 到读未提交的 演变过程和实现原理, 也提了以下MVCC等内容。

看完后感觉和我这篇文章讲的内容基本对的上:将数据库9种锁、3种读、4种隔离级别一次性串联起来,用15张图呈现背后数据库事务背后的并发原理
里面有几句比较重要的话:

  • MYSQL/Innodb的“可重复读级别”只能在“只读”事务中解决幻读问题,但是读写事务还是会幻读
  • 读未提交仍然是包含了写锁的。
  • MVCC只是针对读+写的场景做了优化, 如果是写+写是没法优化的,只能用锁。
  • 范围锁不是指对范围内的每一条记录加锁, 而是整个范围内甚至都不能做插入了,即包含了间隙的锁。

2 全局事务#

这里的全局事务指的是 单个服务 使用多个数据源
核心在于是单个服务,不涉及多服务之间的关联, 视角只有单服务。

XA接口是双向的,能在一个事务管理器和多个资源管理器之间形成通信桥梁,协调多个数据源的一致动作,实现全局事务的统一提交和回滚。
Java基于XA接口衍生出的API叫做JTA(javax.transaction.TrancsactionManager和 XAResource)

注意对于全局事务,调用XA的应用者是可以不需要额外处理的,XA会协助做好以下全局事务的响应操作。

2.1 2PC协议(两阶段提交)#

准备阶段
数据源将需要做的事务操作记录在redolog中,完成了持久化,并仍旧持有锁,保持隔离性

提交阶段
协调者收到了所有数据源的回应后, 给所有数据源发送commit指令,如果有任一失败或者超时,则发送abort回滚指令。

2PC的缺点:

  • 协调者单点问题:协调者挂了其他的数据源都会一直在持有锁的情况下等待
  • 准备阶段的性能问题:整个过程将被最慢的那个数据源所拖累,包括如果连接超时也会影响,导致多余的回滚操作
  • 一致性风险:指令丢失、数据源机器崩溃且无法恢复(FLP不可能原理:如果岱机后无法恢复,那么没有任何分布式协议可以达成一致性)

2.2 3PC协议#

为了解决上面的单点问题和 准备阶段的性能问题,引入3PC协议
将准备阶段扩展为:
CanCommit询问阶段
这个阶段就是为了确认各机器是否还是正常的,如果经过确认都是正常负载的状态,再下发事务操作,这样就能避免被网络超时、不良负载拖累的风险

PreCommit预提交阶段
和之前一样,下发事务后各数据源写入重做日志

DoCommit阶段
这个过程有一个优化, 如果协调者挂了, 数据源迟迟无法收到,就会默认进行事务提交(注意并非默认回滚)

3PC仍然存在网络问题导致的一致性问题。
另外对于2PC,

3 共享事务#

书里说这个不常用,不写了,类似于提供共享的数据连接给不同进程使用,使用同一个事务逻辑

4 分布式事务#

4.1 CAP理论#

  • C一致性: 各节点同一时刻响应结果一致(数据一致)
  • A可用性: 各节点随时随地都要能正常响应,不能存在延迟或者阻塞的情况(快速响应)
  • P分区容忍性:某个节点挂了,其他节点能代替服务

科学家证明CAP只能同时满足2个

  • 放弃分区容忍性P: 意味着分布式系统不成立。这种情况只有类似于Oracle RAC这种数据通过磁盘共享的情况, 虽然是多个实例,但不算分布式。 基本是分布式系统一定都会包含P,否则没有考虑分布式事务的意义
  • 放弃可用性A: 这样可能因为数据同步过程的延迟或者超时,造成系统长时间不可用, 这是不能容忍的
  • 放弃一致性C: 数据有短暂不一致的响应。 放弃C是当前分布式系统的主流选择。 一般都是允许数据在中间过程出错, 但允许在输出时能够修正古来。 因此我们放弃了强一致性,追求“最终一致性”

4.2 BASE(可靠性队列)#

BASE指 基本可用性 + 柔性事务 + 最终一致性, 或者叫做最大努力交付

实现原理是引入一个消息队列,当某个事务动作发生异常时, 在轮询阶段不断重试,直到成功

要求满足幂等性

可靠性事件队列只要第一步完成了,后续就没有失败回滚的概念,只许成功,不许失败。

4.3 TCC事务#

TCC用于解决BASE中无法解决的隔离性问题,因为BASE不允许失败,一定会执行,如果涉及了超售等问题将无法解决。

  • Try: 尝试执行阶段, 会先进行业务可执行的检查,并提前预留好需要扣除的资源(类似于冻结那一块资源,但没有实际去扣)
  • Confirm:执行阶段,这个过程不再做任何检查,直接执行。如果网络出错等缘故则一直重试,符合幂等
  • Cancel:执行完成,释放try阶段中预留的业务资源,也要符合幂等。

和2PC很类似,但TCC是在用户应用代码层面实现的,业务侵入性很高, 而2PC是基础设施层面提供的。

4.4 SAGA事务#

TCC中的缺点在于 try阶段和cancel阶段依赖用户代码实现,但如果你的业务不支持这种操作就麻烦了,比如扣款动作是某个银行做的, 他不支持预扣款的功能。

SAGA会把事务拆成很多个小事务T,按顺序执行, 并根据情况给事务T失败时选择是继续重试T, 还是用补偿事务C来替代重试

这样像银行无法预扣款也无法撤销转账的问题,可以改成自己系统来做中间者做转账操作。

也要引入SAGAlog机制避免长串事务执行过程中崩溃

总结#

其实学习本文时,更重要的是思考为什么要学习这么多的事务概念和原理。在云原生时代。

像华为云提供的很多数据库类型的云服务也都支持了分布式事务的能力,例如

  • 华为云RDS分布式事务
    基于2PC原理实现的MSDTC分布式事务协调器

  • 华为云DDM事务模型:
    这里面的分布式事务模块基于 MySQL XA 协议实现,XA 协议是对 2PC(Two Phase Commit) 事务模型的一种实现。

  • 华为云DWS分布式事务:
    基于强一致性的CSN事务机制,使用GaussDB分布式框架下的一个组件GTM以及从中获取到的CSN值来处理事务。

毕竟云原生应用程序通常由多个微服务组成,因此需要在微服务之间进行通信,并保证事务的一致性。在这种情况下,就需要一种适用业务场景的分布式事务解决方案。比如TCC可以在微服务之间实现分布式事务的ACID特性,而且相对于其他方案,TCC更轻量级,对性能影响更小,但其他方案也有各自的适应场景。

因此,分布式事务与云原生技术有很强的关联,可以帮助云原生应用程序实现高效的分布式事务处理。当使用某个关系型数据库产品时,关注他们的分布式事务处理能力并分析是否适合自己当前的业务场景,是非常重要的,也是本书该章节值得学习的一个理由。