Kyle's Notebook

《DDIA》阅读笔记(七):事务

Word count: 8.2kReading time: 27 min
2021/01/27

《DDIA》阅读笔记(七):事务

本章结构如下:

  • 事务
    • 事务原理
      • ACID 的含义
      • 单对象与多对象事务操作
    • 弱隔离级别
      • 读写竞争
        • 脏写 -> 读-未提交
        • 脏读 -> 读-提交
          • 防止脏读
          • 防止脏写
          • 存在问题
        • 不可重复读 -> 可重复读
          • 实现快照级别隔离
          • 一致性快照的可见性规则
          • 索引与快照级别隔离
      • 写写竞争
        • 原子写操作
        • 显式加锁
        • 自动检测更新丢失
        • 原子比较和设置
        • 冲突解决与复制
      • 多对象冲突
        • 写倾斜
        • 幻读
        • 实体化冲突
    • 强隔离级别
      • 悲观并发控制
        • 实际串行执行
          • 存储过程封装
          • 分区
        • 两阶段加锁(2PL)
          • 实现方式
          • 性能分析
          • 谓词锁
          • 索引区间锁
      • 乐观并发控制
        • 可串行化快照隔离(SSI)
          • 基于过期的条件做决定
          • 检测是否读取过期的 MVCC 对象
          • 检测写是否影响了之前的读
          • 性能问题
            • 跟踪事务读写的粒度
            • 与实际串行执行相比
            • 与两阶段加锁相比

数据存储环境中可能存在各种出错的情况:软硬件失效、应用程序崩溃、节点间链接中断、多客户端并发写入数据覆盖、数据部分更新、边界条件竞争等。通过事务(成功提交或失败回滚)可以简化容错机制,使应用程序安全地重试,不必担心部分失败地情况,但也意味着牺牲系统部分性能和可用性。

几乎所有的关系数据库和一些非关系数据库都支持事务处理,比如 MySQL、PostgreSQL、Oracle、SQL Server 等系统事务实现与 IBM System R 非常相似。

NoSQL 则提供了新的数据模型、内置的复制和分区等手段改进关系模型。同时放弃了事务支持,或替换为更弱的保证。

ACID

原子性(Atomicity)

在出错时中止事务,并将部分完成的写入全部丢弃。

描述了一个包含多个写操作的请求可能发生的情况,当原子事务中的一部分操作写入完成后写入后系统出现故障,则事务会中止,数据库须丢弃或撤销那些局部完成的更改。

一致性(Consistency)

在不同场景有不同含义:

  • 副本一致性以及异步复制模型。

  • 一致性哈希:某些系统用于动态分区再平衡的方法。

  • CAP:表示线性化。

  • ACID:指数据库处于应用程序所期待的“预期状态”。

ACID 中的一致性指对数据有特定的预期状态,任何数据更改必须满足这些状态约束。

  • 更多是应用层属性,应用程序可能借助数据库提供得原子性和隔离性达到一致性。

  • 本质上要求应用层来维护状态一致:数据库只能保证特定类型的恒等约束检查(外键、唯一等)以及存储,而应用程序需要定义有效/无效状态。

隔离性(Isolation)

意味着多个并发执行的事务相互隔离、不能交叉,由数据库保证当事务提交时,其结果与串行执行完全相同(当多个客户端同时访问数据库的相同记录,会遇到并发问题,即竞争条件)。

实践中由于性能问题很少使用可串行化隔离(Oracle 的串行化功能本质是快照隔离)。

持久性(Durability)

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

  • 对于单节点数据库,持久性意味着数据已被写入非易失性存储;对于支持远程复制的数据库,持久性意味着已成功复制到多个节点。只有写入或复制完成,才能报告事务提交成功。

  • 没有任何一种技术可以确保绝对的持久性,只能多种手段结合使用降低风险。

单/多对象事务操作

多对象事务要求确定知道事务包含的读写操作。

  • 对于关系数据库,客户端与数据库服务器建立 TCP 连接,对于特定的某个连接,SQL 语句 BEGIN TRANSACTION 和 COMMIT 之间的操作属于同一个事务;

  • 许多非关系数据库不会将这些操作组合在一起,即使支持多对象 API(批量插入、更新等),也不具备事务语意。

单对象写入

对于单个对象的更新,需要确保写入的对象完整(可被解析的)、唯一(不存在新旧值混杂),写入过程中被读取也不会出现部分的值。可对每个对象采用加锁的方式、每次只允许一个线程访问实现隔离。

某些数据库还提供了高级的原子操作(自增、比较-设置等),无需在应用层实现“读取-修改-写回”的过程,只有当前值没有被他人修改时才执行写入。

单对象操作可以有效防止多个客户端并发修改同一对象时的更新丢失问题。

多对象事务

由于出现跨分区的多对象事务难以实现,而且在高可用或极致性能场景下由很多负面影响,很多分布式数据系统不支持多对象事务。

要求写入多个不同的对象并进行协调的情况:

  • 对于关系数据模型,表中的某行可能是另一个表中的外键。

  • 对于文档数据模型,如待更新字段在同一个文档中,可视为单个对象,此时不需要多对象事务。

  • 除了纯粹键值存储以外,几乎所有其他系统都支持二级索引,每次更改值时都需要同步更新索引。

此时需要事务的原子性保证,否则会出现部分更新、数据不同步的情况。

处理错误与中止

如果发生了意外、所有操作被中止,之后可以安全地重试:即违反原子性/隔离性/持久性时,可完全放弃整个事务。

一些系统无法遵循,比如无主节点的数据复制会在尽力而为的基础上尝试多做些工作,但遇上错误并不会撤销已完成操作,需要应用程序负责恢复。

重试中止的事务存在以下问题:

  • 重试补偿:如果事务实际执行成功,返回给客户端的消息在网络传输时发生意外,重试就会导致重复执行,此时需要额外的应用级重复数据删除机制。

  • 重试退避:如果由于系统超负荷导致错误,需要设置重试上限,比如指数回退,同时解决系统过载本身的问题。

  • 区分重试:由临时性故障(死锁、隔离违例、网络闪断、节点切换)所导致的错误需要重试,但永久性故障(违反约束)则重试无意义。

  • 外部影响:哪怕事务被中止,事务在数据库之外的副作用已生效(跨系统同时提交或放弃,可采用二阶段提交)。

  • 重试失败:如果客户端进程在重试过程中也发生失败,没有其他机制继续重试,则待写入的数据可能会因此而丢失。

弱隔离级别

如果两个事务操作不存在依赖关系的不同数据,可以安全地并行执行。

只有出现某个事务修改数据而另一个事务同时要读取该数据,或者两个事务同时修改相同数据时,才会引发并发问题(竞态条件)。这类问题很难通过测试发现,只会在特定时刻触发,难以稳定重现,对于大型应用也难以推理分析。

数据库一直试图通过事务隔离来对应用开发者隐藏内部的各种并发问题,如使用可串行化级别的隔离,则不存在并发问题,但会严重影响性能,因此更倾向于使用弱隔离级别。

读写竞争

脏写 -> 读-未提交

即可以访问未提交事务修改的数据,不能防止脏读。

脏读 -> 读-提交

要求读数据库时只能看到已成功提交的数据(防止脏读),写数据库时只会覆盖已成功提交的数据(防止脏写)。

基于加锁方式的“读-提交”隔离可能发生死锁。

防止脏读

脏读:某个事务已经完成部分数据写入,但事务尚未提交(或中止),另一个事务可访问未提交的数据。当有以下需求时,需要防止脏读:

  • 如果事务需更新多个对象,意味着另一个事务可能会看到部分更新,而非全部。

  • 如果事务发生中止,则所有写入操作都需要回滚。

解决方法:当事务想修改某个对象(例如行或文档)时,它必须首先获得该对象的锁;然后一直持有锁直到事务提交(或中止)。在给定时刻只有一个事务可以拿到特定对象的锁,其他事务只能等该事务提交或中止后才能获得锁继续执行。

防止脏写

脏写:当两个事务同时尝试更新相同的对象,先前的写入是尚未提交事务的一部分,而被后写的事务覆盖。如果事务需要更新多个对象,脏写会带来非预期的错误结果。

解决方法:对于每个待更新的对象,数据库都会维护其旧值和当前持锁事务将要设置的新值两个版本。在事务提交之前所有其他读操作都读取旧值;仅当写事务提交之后,才会切换到读取新值。

但在读-提交级别的隔离中,无法解决两个客户端竞争同一个计数器执行增量的情况,因为这属于更新丢失(覆盖)。

存在问题

“读-提交”的隔离级别存在 不可重复读取(nonrepeatable read)读倾斜(read skew) 的问题:比如 A 账号转账到 B 账号的场景,存在读取到 B 账号收到转账前、A 账号发出转账后的情况,此时两边总额减少了。其他不可容忍的暂时性不一致场景:

  • 数据备份:备份任务要复制整个数据库需要很长时间,在此过程中允许继续写入数据,因此得到镜像可能包含部分旧版本数据和部分新版本数据。

  • 分析查询与完整性检查:有时查询可能会扫描几乎大半个数据库,如果在不同时间点观察数据库,可能会返回无意义的结果。

不可重复读 -> 可重复读

快照级别隔离要求每个事务都从数据库的一致性快照中读取,事务一开始看到是最近提交的数据(即使数据随后可能被另一个事务更改),保证每个事务都只看到该特定时间点的旧数据。

  • 适用于长时间运行的只读查询:如备份和分析,避免数据在查询过程中变化时读取到变化的数据。

  • 快照级别隔离对于只读事务特别有效,但在具体实现时不同数据库对此有不同命名:Orcale 称之为可串行化,PostgreSQL、MySQL 称之为可重复读。

快照级别隔离

通常采用写锁来防止脏写,正在进行写操作的事务会阻止同一对象上的其他事务,而读取不需要加锁。可以在处理正常写入的同时,在一致性快照上执行长时间的只读查询,两者没有锁竞争。

多个事务可能在不同时间点查看数据库状态,所以数据库保留了对象多个不同的提交版本,因此也被称为 多版本并发控制(MVCC)

  • 如果只提供读-提交级别隔离,则只保留对象的两个版本:一个已提交的旧版本和尚未提交的新版本。支持快照级别隔离的存储引擎可直接采用 MVCC 来实现读-提交隔离:

    • “读-提交”级别下对每个不同的查询单独创建一个快照。

    • 快照级别隔离则是使用一个快照来运行整个事务。

  • 表中的每行都有 created_by 字段,包含了创建该行的事务 ID。每行还有初始为空的 deleted_by 字段。如果事务要删除某行,只是将 deleted_by 字段设置为请求删除的事务 ID。事后 确定没有其他事务引用该标记删除的行 时,数据库的垃圾回收进程才去真正删除并释放存储空间。

  • 当事务读数据库时,通过事务 ID 可以决定对象可见/不可见。

一致性快照的可见性规则

长时间运行的事务可能会使用快照很长时间,从其他事务的角度来看,它可能在持续访问正在被覆盖或删除的内容。由于没有就地更新、而是每次修改总创建一个新版本,因此数据库可以以较小的运行代价来维护一致性快照。

事务 ID 数据不可见:

  • 每个事务开始时,数据库列出尚在进行中的其他事务(未提交或中止),并忽略这些事务完成的写入(尽管之后可能被提交)。

  • 所有中止事务所做的修改。

  • 晚于当前事务的事务 ID 所做的任何修改(不管是否完成提交)。

事务 ID 数据可见:

  • 事务开始的时刻,创建该对象的事务已经完成了提交。

  • 对象没有被标记为删除;或者即使标记了,但删除事务在当前事务开始时还没有完成提交。

索引与快照级别隔离

对于多版本的数据库要支持索引,可以把索引指向对象的所有版本(如把同一对象的不同版本放在一个内存页面上),并过滤对当前事务不可见版本。当后台的垃圾回收进程决定删除某个旧对象版本时也把对应的索引条目删除。

CouchDB、Datomic 和 LMDB 使用另一种方法,主体结构是 B-tree,并采用了追加/写时复制的技术:当需要更新时都创建一个新的修改副本拷贝必要的内容(新的 B-tree root,代表该时刻数据库的一致性快照),让父结点或者递归向上直到树的 root 结点都指向新创建的结点,而忽略不受影响的页面。因此在查询时不需要根据事务 ID 过滤。

写写竞争(更新丢失)

“读-提交”和“快照级别隔离”都是解决 只读事务遇到并发写时 数据可见性问题。

当应用程序从数据库读取某些值,根据应用逻辑做出修改然后写回新值。有两个事务在同样的数据对象上执行类似操作时,由于隔离性,第二个写操作不包括第一个事务修改后的值,最终会导致第一个事务的修改值可能会丢失。

更新丢失的其他场景:

  • 递增计数器。

  • 对某复杂对象的一部分内容执行修改,如 JSON 文档。

  • 两个用户同时编辑页面。

使用一级封锁协议可以解决 更新丢失 问题。

原子写操作

数据库内置原子操作可避免“读-修改-写回”操作,确保并发安全比如:

1
2
3
UPDATE table 
SET value = value + 1
WHERE key = 'foo';
  • 通常采用对读取对象加独占锁实现,确保更新提交前不会被其他事务读取,有时也被称为 游标稳定性。另一种实现方式是强制所有原子操作都在单线程上执行。

  • 基于 ORM 框架很容易产生非“读取-修改-写回”的应用层代码,导致无法使用数据库提供的原子操作,可能会埋下难以发现的潜在错误。

显式加锁

由应用程序显式锁定待更新的对象,然后执行“读-修改-写回”操作。此时如果有其他事务尝试同时读取对象,则必须等待当前正在执行得序列全部完成。

自动检测更新丢失

先让操作并发执行,如果事务管理器检测到了更新丢失风险则中止当前事务,并强制回退到安全的“读-修改-写回”方式。

可借助快照级别隔离高效执行检查,如 PostgreSQL 的可重复读、Oracle 的可串行化、SQL Server 的快照级别隔离都能检测冲突并中止违规事务,但 MySQL InnoDB 的可重复读不支持。

原子比较和设置

对于不支持事务的数据库,有时会支持原子“比较和设置”操作以避免更新丢失:只有在上次读取的数据没有发生变化时才允许更新,否则回退到“读-修改-写回”方式:

1
2
3
4
UPDATE table
SET value = 'new content'
WHERE id = 'id'
AND value = 'old content'

冲突解决与复制

对于多副本的数据库,由于多节点上的数据副本、不同的节点可能会并发修改数据,必须采取一些额外的措施来防止丢失更新。

  • 加锁和原子修改都有个前提即只有一个最新的数据副本。多主节点或者无主节点的多副本数据库支持多个并发写,且通常以异步方式来同步更新,会有多个最新的数据副本,所以加锁和原子修改不适用。

  • 多副本数据库通常支持多个并发写,然后保留多个冲突版本,由应用层逻辑或依靠特定的数据结构解决多版本问题。

  • 如果操作可交换,由于顺序无关,原子操作在多副本情况下也可以工作。

  • 默认情况下用最后写入获胜(LWW)的方法解决冲突会丢失更新。

多对象冲突

写倾斜

写倾斜:约束条件为两个对象不能被同时修改,当由两个事务同时分别修改这两个对象都能安全通过检查、各自成功提交,则发生了写倾斜:

  • 可视为更广义的更新丢失问题。如果两个事务读取相同的一组对象,然后更新其中一部分:不同的事务可能更新不同的对象,则可能发生写倾斜;而不同的事务如果更新的是同一个对象,则可能发生脏写或更新丢失。

  • 由于涉及多个对象,无法使用单对象原子操作规避。

  • 基于快照级别隔离来实现更新丢失自动检测也无法解决,比如 MySQL InnoDB 可重复读,Oracle 可串行化和 SQL Server 的快照级别隔离都不支持检测写倾斜问题。解决写倾斜问题要求真正的可串行化。

  • 某些数据库可自定义约束条件,然后由数据库代为检查、执行约束,比如唯一性,外键约束或者限制特定值。但涉及对象约束则大多数数据库都无法支持。开发者可通过触发器和物化视图来自行实现。

  • 如果不能使用可串行化级别隔离,还可以对事务依赖的行来显式的加锁:

1
2
3
4
5
6
7
8
BEGIN TRANSACTION;

--- FOR UPDATE 会通知数据库对返回的所有结果自动加锁
SELECT ... WHERE ... FOR UPDATE;

UPDATE ... WHERE ...;

COMMIT;

幻读

写倾斜主要出现在两个事务请求的数据范围存在重叠,或两个事务都可单独安全执行、但无法叠加执行的情况,这种情况无法通过对单对象加锁解决。其出现的模式:

  • 访问满足条件的行。

  • 根据查询结果,应用层代码来决定下一步的操作。

  • 如果应用程序决定继续执行,即发起数据库写入。

幻读:第三步的写入操作会改变第二步的前提条件,即提交写入后再次执行第一步的结果与第一次执行时的结果不同(使得另一个曾因为满足条件而开启事务的前提条件被破坏)。

  • 对于破坏条件中包含的记录,可以通过 SELECT FOR UPDATE 解决。

  • 但如果是 先检查不满足条件的行、再添加符合条件的行,则无法通过这种范围锁来解决,因为要加锁的对象不存在。

实体化冲突

如果先决条件查询结果为空、没有对象可以加锁,还可以人为加入可加锁的对象:比如对于时间范围重叠问题,可以创建表格提前创建好实体与时间范围所有可能的组合以便于加锁,但并不存储这些时间范围与对象组合的具体占用信息。

这种做法把并发控制机制降级为数据模型的思路总是不够优雅,而且也容易出错。因此更推荐使用可串行化隔离方案。

强隔离级别

对于各种隔离级别,存在以下问题:

  • 隔离级别通常难以理解,而且不同的数据库的实现不尽一致;

  • 检查应用层的代码往往很难判断在特定的隔离级别下是否安全,难以预测所有可能并发情况。

  • 还缺乏好的工具来帮助检测竞争状况,静态分析难以应对特定场景下、不确定发送的并发性问题。

可串行化 为最强的隔离级别,即确保并行执行的事务最终结果与串行执行相同。目前提供可串行化的数据库使用三种方式实现:

  • 严格按照串行顺序执行;

  • 两阶段锁定;

  • 乐观并发控制。

悲观并发控制

  • 两阶段加锁是典型的悲观并发控制机制:假设冲突发生必然出错,则直接放弃,采用等待方式直到绝对安全。相当于多线程编程中的互斥锁。

  • 实际串行执行是极端悲观的选择:事务执行等价于事务对整个数据库(或分区)持有互斥锁。

实际串行执行

最直接的方法是避免并发,在单线程上顺序执行事务,回避诸如检测、防止事务冲突等间题。

目前转向单线程执行事务主要是出于两方面的考虑:

  • 内存越来越便宜,许多应用可将整个活动数据集加载到内存中。

  • OLTP 事务通常执行很快,只产生少量的读写操作。

  • 运行时间较长的分析查询则通常是只读的,可以在一致性快照运行,不需要运行在串行主循环中。

当满足以下约束条件,串行执行事务可以实现串行化隔离:

  • 简短而高效,否则一个缓慢的事务会影响整体执行性能。

  • 仅限于活动数据集完全可以加载到内存的场景,对于很少访问数据被移到内存,当单线程事务访问时会严重拖慢性能。

  • 写入吞吐量必须足够低,才能在单个 CPU 核上处理;否则就需要采用分区,最好没有跨分区事务(或占比很小)。

存储过程封装

事务机制设计初衷是襄括用户的、所有操作序列。由于人们做出决定并回应的速度较慢,如果所有操作都等待同时还要支持潜在大量并发需求,则系统大部分时间都将处于空闲中。

OLTP 应用程序避免在事务执行中等待用户交互从而使事务非常简洁,Web 应用的着事务会在一个 HTTP 请求中提交,而不会跨越多个请求。

  • 交互式事务:应用程序来提交查询,读取结果,可能会根据前一个查询的结果来进行其他查询,依此类推。请求与结果在应用层代码和数据库服务器之间来回交互。

  • 存储过程:为了获得足够的吞吐性能,系统需要能够同时处理多个事务。因此单线程串行执行的系统不支持交互式的多语句事务,应用程序必须提交整个事务代码作为存储过程打包发送到数据库。

交互式事务与存储过程

存储过程的优点:

  • 最新的存储过程已放弃 PL/SQL,使用通用的编程语言。

  • 存储过程与内存式数据存储使得单线程上执行所有事务变得可行。不需要等待 I/O,避免加锁开销等复杂的并发控制机制,可得到高吞吐量。

  • 可借助存储过程执行复制:如 VoltDB 在每个副本上执行相同的存储过程(但要求操作必须是确定性的)。

但也存在问题:

  • 开发语言因数据库厂商而异。

  • 在数据库中运行代码难以管理:调试困难,版本控制部署复杂,测试不便,不容易与指标监控系统集成。

  • 数据库被多个应用共享,要求性能更高,存储过程涉及不理想要比同样低效的应用服务器代码带来更大麻烦。

性能、兼容性问题被解决,但可维护性问题未解决。

分区

串行执行的吞吐量被限制在单机单个 CPU 核。只读事务可在单独的快照上执行,但对于高写入需求的应用程序,单线程事务处理容易成为瓶颈。因此需要对数据库分区、扩展到多 CPU 多核。

  • 通过为每个 CPU 核分配一个分区、每个事务只在单分区中读写数据,可提升总体事务吞吐量,比如 VoltDB。

  • 对于跨分区的事务,数据库必须在涉及的所有分区之间协调事务。存储过程需要跨越所有分区加锁执行以确保整个系统的可串行化,性能比单分区慢得多。

  • 事务是否能只在单分区上执行,很大程度上取决于应用层的数据结构。比如键值数据比较容易切分,但带多个二级索引的数据需要大量的跨区协调,不适合在多分区上执行事务。

乐观并发控制

假设冲突可能发生,事务会继续执行而不是中止;当事务提交时数据库检查是否确实发生了冲突,是则中止事务并重试。

  • 事务中的所有读取操作都是基于数据库的一致性快照。

  • 如果系统还有足够的性能提升空间,且事务之间竞争不大,乐观并发控制会比悲观方式高效很多,通过可交换的原子操作还可以减少一些竞争情况。

  • 如果冲突很多,大量的事务必须中止,当系统已接近其最大吞吐量,反复重试事务会使系统性能变得更差。

可串行化快照隔离(SSI)

两阶段加锁虽然可以保证串行化,但由于串行执行,性能较差且无法扩展;弱级别隔离虽然性能不错,但容易引发各种边界条件(如更新丢失,写倾斜,幻读等)。

可串行化快照隔离算法(SSI)提供了完整的可串行性保证,而性能相比于快照隔离损失很小。

基于过期条件决定

事务是基于某些前提条件而决定采取行动, 在事务开始时条件成立,而当事务要提交时,数据可能已经发生改变,条件不再成立。

在执行查询时,数据库无法预知应用层逻辑如何使用查询结果,假定对查询结果的任何变化都应使写事务失效。

数据库检测事务是否会修改其他事务的查询结果,并在此情况下中止写事务:

  • 读取前:读取是否作用于一个(即将)过期的 MVCC 对象(读之前已经有未提交的写入)。

  • 写入前:检查写入是否影响即将完成的读取(读取之后又有新的写入)。

检测过期

检测是否读取过期的 MVCC 对象:快照隔离通常采用多版本并发控制技术实现,当事务从 MVCC 数据库一致性快照读取时会忽略在创建快照时尚未提交的事务写入。

为防止这种异常:

  • 数据库需要跟踪由于 MVCC 可见性规则而被忽略的写操作,在事务提交时会检查是否存在一些当初被忽略的写操作现在已经完成了提交,是则中止当前事务。

  • 读事务没有倾斜风险,不需要中止。通过减少不必要的中止,SSI 可以高效支持需要在一致性快照中运行很长时间的读事务。

检测写影响

检测写是否影响了之前的读:为了避免在读取数据之后另一个事务修改了数据的情况,2PL 的索引区间锁可锁定与某个查询条件匹配的所有行。

而 SSI 锁则不会阻塞其他事务:

  • 数据库可通过索引条目记录两个事务都查询了相同的结果(或在表级别跟踪此信息)。该额外记录只需保留很小一段时间,当并发的所有事务都处理提交或中止后就可以丢弃。

  • 当另一个事务尝试修改时,首先检查索引,从而确定是否最近存在读取该数据的其他事务。类似在受影响的字段范围上获取写锁,但并不会阻塞读取,而直到读事务提交时才进一步通知变化状况。

性能问题
  • 有时读取过期的数据不会造成太大影响,可以确信执行的最终结果是可串行化的。

  • 事务中止的比例会显著影响 SSI 的性能表现。如运行很长时间的事务,读取和写入了大量数据,产生冲突并中止的概率就会增大,所以 SSI 要求读-写型事务要简短。但容忍度比两阶段加锁与串行执行高。

  • 权衡跟踪事务读、写的粒度:如果详细跟踪事务操作,可以准确推测有哪些事务受到影响、需要中止,但元数据记录开销可能很大;而粗粒度的记录则速度占优,但可能会扩大受影响事务范围。

  • 与实际串行执行相比:可以突破单个 CPU 核的限制,如将冲突检测分布在多台机器上,可提高总体吞吐量。数据可能跨多台机器进行分区,事务也可以在多个分区上读、写数据并保证可串行化隔离。

  • 与两阶段加锁相比:由于不需要等待其他事务所持有的锁,和快照隔离一样,读写通常不会互相阻塞。使得查询延迟更加稳定、 可预测。在一致性快照上执行只读查询不需要任何锁,有利于提高读密集的负载。

CATALOG
  1. 1. 《DDIA》阅读笔记(七):事务
    1. 1.1. ACID
      1. 1.1.1. 原子性(Atomicity)
      2. 1.1.2. 一致性(Consistency)
      3. 1.1.3. 隔离性(Isolation)
      4. 1.1.4. 持久性(Durability)
    2. 1.2. 单/多对象事务操作
      1. 1.2.1. 单对象写入
      2. 1.2.2. 多对象事务
      3. 1.2.3. 处理错误与中止
    3. 1.3. 弱隔离级别
      1. 1.3.1. 读写竞争
        1. 1.3.1.1. 脏写 -> 读-未提交
        2. 1.3.1.2. 脏读 -> 读-提交
          1. 1.3.1.2.1. 防止脏读
          2. 1.3.1.2.2. 防止脏写
          3. 1.3.1.2.3. 存在问题
        3. 1.3.1.3. 不可重复读 -> 可重复读
          1. 1.3.1.3.1. 快照级别隔离
          2. 1.3.1.3.2. 一致性快照的可见性规则
          3. 1.3.1.3.3. 索引与快照级别隔离
      2. 1.3.2. 写写竞争(更新丢失)
        1. 1.3.2.1. 原子写操作
        2. 1.3.2.2. 显式加锁
        3. 1.3.2.3. 自动检测更新丢失
        4. 1.3.2.4. 原子比较和设置
        5. 1.3.2.5. 冲突解决与复制
      3. 1.3.3. 多对象冲突
        1. 1.3.3.1. 写倾斜
        2. 1.3.3.2. 幻读
        3. 1.3.3.3. 实体化冲突
    4. 1.4. 强隔离级别
      1. 1.4.1. 悲观并发控制
        1. 1.4.1.1. 实际串行执行
          1. 1.4.1.1.1. 存储过程封装
          2. 1.4.1.1.2. 分区
      2. 1.4.2. 乐观并发控制
        1. 1.4.2.1. 可串行化快照隔离(SSI)
          1. 1.4.2.1.1. 基于过期条件决定
          2. 1.4.2.1.2. 检测过期
          3. 1.4.2.1.3. 检测写影响
          4. 1.4.2.1.4. 性能问题