DDIA读书笔记 | 第七章:事务


前言

事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。
从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功 提交(commit),要么失败 中止(abort)或 回滚(rollback)。

本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入 并发控制 的领域,讨论各种可能发生的竞争条件,以及数据库如何实现 读已提交(read committed),快照隔离(snapshot isolation) 和 可串行化(serializability) 等隔离级别。


一、事务的相关概念

1.1 ACID含义

不符合 ACID 标准的系统有时被称为 BASE,它代表 基本可用性(Basically Available),软状态(Soft State) 和 最终一致性(Eventual consistency)

  • 原子性
  • 定义特征:
    能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。 或许 可中止性(abortability) 是更好的术语。

    原子性并 不是关于 并发(concurrent) 的。
    原子性简化了这个问题:如果事务被 中止(abort),应用程序可以确定它没有改变任何东西,所以可以安全地重试。

  • 一致性
  • 概念: 对数据的一组特定约束必须始终成立。即 不变式(invariants)。

    原子性,隔离性和持久性是数据库的属性(数据库只管理存储),而一致性(在 ACID 意义上)是应用程序的属性

  • 隔离性
  • 概念:
    同时执行的事务是相互隔离的:它们不能相互冒犯。
    传统的数据库教科书将隔离性形式化为 可串行化(Serializability),这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。

    作用:
    数据库确保当多个事务被提交时,结果与它们串行运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的

    缺点:
    实践中很少会使用可串行的隔离,因为它有性能损失。在 Oracle 中有一个名为 “可串行的” 隔离级别,但实际上它实现了一种叫做 快照隔离(snapshot isolation) 的功能,这是一种比可串行化更弱的保证【8,11】。我们将在 “弱隔离级别” 中研究快照隔离和其他形式的隔离。

  • 持久性
  • **持久性** 是一个承诺,即**一旦事务成功完成**,即使发生硬件故障或数据库崩溃,写入的任何数据也**不会丢失**。

    在单节点数据库中
    持久性通常意味着数据已被写入非易失性存储设备,如硬盘或 SSD。它通常还包括**预写日志(WAL)**或类似的文件(请参阅 “让 B 树更可靠”),以便在磁盘上的数据结构损坏时进行恢复。

    在带复制的数据库中:
    持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交

    1.2 单对象和多对象操作

    简介

    一个事务读取另一个事务的未被执行的写入(“脏读”): 违反隔离性。

    多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中,通常基于客户端与数据库服务器的 TCP 连接:在任何特定连接上,BEGIN TRANSACTION 和 COMMIT 语句之间的所有内容,被认为是同一事务的一部分

    这并不完美。如果 TCP 连接中断,则事务必须中止。如果中断发生在客户端请求提交之后,但在服务器确认提交发生之前,客户端并不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组,这个标识符并未绑定到特定 TCP 连接。后续再 “数据库的端到端原则” 一节将回到这个主题。

    **另一方面,**许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象 API(例如,某键值存储可能具有在一个操作中更新几个键的 multi-put 操作),但这并不一定意味着它具有事务语义:该命令可能在一些键上成功,在其他的键上失败,使数据库处于部分更新的状态。

    1.2.1 单对象写入

    当单个对象发生改变时,原子性和隔离性也是适用的。

    一些数据库也提供更复杂的原子操作,例如自增操作。同样流行的是 比较和设置(CAS, compare-and-set) 操作,仅当值没有被其他并发修改过时,才允许执行写操作。

    事务通常被理解为,将多个对象上的多个操作合并为一个执行单元的机制

    1.2.2 多对象事务的需求

    许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍

    没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题。

    1.2.3 处理错误和终止

  • 事务:宁为玉碎,不为瓦全
  • 无主复制的数据存储: “尽力而为” 的基础上进行工作。可以概括为 “数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情 “ —— 所以,**从错误中恢复是应用程序的责任**。
  • 尽管重试一个中止的事务是一个简单而有效的错误处理机制,但它并不完美:

  • 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障(所以客户端认为提交失败了),那么重试事务会导致事务被执行两次 —— 除非你有一个额外的应用级去重机制。
  • 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,可以限制重试次数,使用指数退避算法,并单独处理与过载相关的错误(如果允许)。 仅在临时性错误(例如,由于死锁,异常情况,临时性网络中断和故障切换)后才值得重试。在发生永久性错误(例如,违反约束)之后重试是毫无意义的。
  • 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,两阶段提交(2PC, two-phase commit) 可以提供帮助(“原子提交与两阶段提交” 中将讨论这个问题)。
  • 如果客户端进程在重试中失效,任何试图写入数据库的数据都将丢失
  • 二、弱隔离级别

    ——>如果两个事务不触及相同的数据,它们可以安全地 并行(parallel) 运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。

    ——>并发 BUG 很难通过测试找到,因为这样的错误只有在特殊时序下才会触发。

    ——>出于这个原因,数据库一直试图通过提供 事务隔离(transaction isolation) 来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:可串行的(serializable) 隔离等级意味着数据库保证事务的效果如同串行运行(即一次一个,没有任何并发)。

    ——>但是可串行的隔离会有性能损失!!!

    ——>所以,系统通常使用较弱的隔离级别来防止一部分而不是全部的并发问题。

    在本节中,我们将看几个在实践中使用的弱(非串行的,即 nonserializable)隔离级别,并详细讨论哪种竞争条件可能发生也可能不发生,以便你可以决定什么级别适合你的应用程序。一旦我们完成了这个工作,我们将详细讨论可串行化(请参阅 “可串行化”)。

    2.1 读已提交

    最基本的事务隔离级别是 读已提交(Read Committed),它提供了两个保证:

    1. 从数据库读时,只能看到已提交的数据(没有 脏读,即 dirty reads)。
    2. 写入数据库时,只会覆盖已经写入的数据(没有脏写,即 dirty writes)。

    某些数据库支持甚至更弱的隔离级别,称为 读未提交(Read uncommitted)。它可以防止脏写,但不防止脏读。

    2.1.1 脏读

    一个事务已经将一些数据写入数据库,但事务还未提交或中止。另一个事务可以看到未提交的数据

    为什么要防止脏读,有几个原因:

  • 如果事务需要更新多个对象,脏读意味着另一个事务可能会**只看到一部分更新**。
  • 如果事务中止,则所有写入操作都需要回滚。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
  • 2.1.2 脏写

    概念:
    如果两个事务同时尝试更新数据库中的相同对象。
    先前的写入是尚未提交事务的一部分,后面的写入会覆盖一个尚未提交的值。

    解决方案:
    通常用延迟第二次写入,直到第一次写入事务提交或中止为止。

    缺点:
    读已提交可以防止脏写,但不能防止两个计数器增量之间的竞争状态。

    2.1.3 读已提交的实现:

  • 防止脏写:
  • 行锁,当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。

  • 防止脏读:
  • 行锁
    一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。

    但要求读锁在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读事务等到这个慢写入事务完成。这会损失只读事务的响应时间,并且不利于可操作性。

    大多数数据库使用以下方式防止脏读:
    对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。

    2.2 快照隔离和可重复读

    2.2.1 问题提出
    爱丽丝在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元。现在有一笔事务从她的一个账户转移了 100 美元到另一个账户。如果她非常不幸地在事务处理的过程中查看其账户余额列表,她可能会在收到付款之前先看到一个账户的余额(收款账户,余额仍为 500 美元),在发出转账之后再看到另一个账户的余额(付款账户,新余额为 400 美元)。对爱丽丝来说,现在她的账户似乎总共只有 900 美元 —— 看起来有 100 美元已经凭空消失了。

    这种异常被称为 不可重复读(nonrepeatable read) 或 读取偏差(read skew)

    读已提交的隔离条件下,不可重复读 被认为是可接受的:Alice 看到的帐户余额时确实在阅读时已经提交了。

    2.2.2 适用场景:
    虽然这种情况不是持续性,但是有些情况下,不能容忍这种暂时的不一致:

  • 备份
  • 进行备份需要复制整个数据库,对大型数据库而言可能需要花费数小时才能完成。备份进程运行时,数据库仍然会接受写入操作。因此 备份可能会包含一些旧的部分和一些新的部分。如果从这样的备份中恢复,那么不一致(如消失的钱)就会变成永久的。
  • 分析查询和完整性检查
  • 有时可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见,也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。

    2.2.3 解决方案:快照隔离
    每个事务都从数据库的 一致快照中读取 —— 也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
    事务可以看到数据库在某个特定时间点冻结时的一致快照。

    2.2.4 实现:
    与读取提交的隔离类似,快照隔离的实现通常使用写锁来防止脏写,这意味着进行写入的事务会阻止另一个事务修改同一个对象。但是读取不需要任何锁定。

    特点: 读不阻塞写,写不阻塞读

    2.2.4 总结:
    数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它同时维护着单个对象的多个版本,所以这种技术被称为 多版本并发控制(MVCC)

    读已提交:
    保存两个版本即可,提交的版本和被覆盖但尚未提交的版本。

    支持快照隔离的存储引擎通常也使用 MVCC 来实现 读已提交 隔离级别。一种典型的方法是 读已提交 为每个查询使用单独的快照,而 快照隔离 对整个事务使用相同的快照

    2.2.5 观察一致性快照的可见性规则:
    当一个事务从数据库中读取时,事务 ID 用于决定它可以看见哪些对象,看不见哪些对象。通过仔细定义可见性规则,数据库可以向应用程序呈现一致的数据库快照。工作如下:

    1. 在每次事务开始时,数据库列出当时所有其他(尚未提交或尚未中止)的事务清单,即使之后提交了,这些事务已执行的任何写入也都会被忽略
    2. 被中止事务所执行的任何写入都将被忽略。
    3. 由具有较晚事务 ID(即,在当前事务开始之后开始的)的事务所做的任何写入都被忽略,而不管这些事务是否已经提交。(严格按照ID顺序来执行)
    4. 所有其他写入,对应用都是可见的。

    换句话说,如果以下两个条件都成立,则可见一个对象:

  • 读事务开始时,创建该对象的事务已经提交。
  • 对象未被标记为删除,或如果被标记为删除,请求删除的事务在读事务开始时尚未提交。
  • 2.2.6 索引和快照隔离:
    索引如何在多版本数据库中工作?一种选择是使索引简单地指向对象的所有版本,并且需要索引查询来过滤掉当前事务不可见的任何对象版本。当垃圾收集删除任何事务不再可见的旧对象版本时,相应的索引条目也可以被删除。

    2.2.7 可重复度与命名混淆:
    快照隔离是一个有用的隔离级别,特别对于只读事务而言。

    在 Oracle 中称为 可串行化(Serializable) 的,在 PostgreSQL 和 MySQL 中称为 可重复读

    2.3 防止丢失更新

    读已提交快照隔离 级别,主要保证了 只读事务在并发写入时 可以看到什么
    却忽略了两个事务并发写入的问题(我们只考虑了脏写,没考虑写冲突)。

    并发的写入事务之间的典型冲突:

  • 丢失更新:以两个并发计数器增量为例。
  • 2.3.1:原子写
    原子操作通常通过在读取对象时,获取其上的排它锁来实现。 另一个选择是简单地强制所有的原子操作在单一线程上执行。

    2.3.2:显示锁定
    如果数据库的内置原子操作没有提供必要的功能,防止丢失更新的另一个选择是让应用程序显式地锁定将要更新的对象。然后应用程序可以执行读取 - 修改 - 写入序列,如果任何其他事务尝试同时读取同一个对象,则强制等待,直到第一个 读取 - 修改 - 写入序列 完成。

    2.3.3:自动检测丢失的更新
    原子操作和锁是通过强制 读取 - 修改 - 写入序列 按顺序发生,来防止丢失更新的方法。
    另一种方法是允许它们并行执行,如果事务管理器检测到丢失更新,则中止事务并强制它们重试其 读取 - 修改 - 写入序列。

    优点:
    数据库可以结合快照隔离高效地执行此检查。
    事实上,PostgreSQL 的可重复读,Oracle 的可串行化和 SQL Server 的快照隔离级别,都会自动检测到丢失更新,并中止惹麻烦的事务。但是,MySQL/InnoDB 的可重复读并不会检测 丢失更新。一些作者认为,数据库必须能防止丢失更新才称得上是提供了 快照隔离,所以在这个定义下,MySQL 下不提供快照隔离

    2.3.4:比较并设置(CAS)
    此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取 - 修改 - 写入序列。

    但是,如果数据库允许 WHERE 子句从旧快照中读取,则此语句可能无法防止丢失更新,因为即使发生了另一个并发写入,WHERE 条件也可能为真。

    2.3.5:冲突解决和复制

  • 最后写入胜利(LWW)的冲突解决方法很容易丢失更新,如 “最后写入胜利(丢弃并发写入)” 中所述。但LWW 是许多复制数据库中的默认方案。
  • 2.4 写入偏斜与幻读

    在多个事务更新同一个对象的特殊情况下,就会发生1 脏写2 丢失更新(取决于时序)。

    3 写偏差:
    事务并发进行时,两个事务正在更新两个不同的对象(Alice 和 Bob 各自的待命记录)

    写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入偏差。

    解决方案:
    自动防止写入偏差需要真正的可串行化隔离。

    FOR UPDATE 告诉数据库锁定返回的所有行以用于更新。

    2.4.1 写入偏差例子:

  • 会议室预订系统
  • 多人同时提交多某个教室的预定,在快照隔离下不安全。

  • 多人游戏
  • 我们使用锁可以防止丢失更新(也即是可以确保两个玩家不能同时移动同一个棋子),但是锁不能保证两个玩家将两个不同的棋子移动到同一个地方,或者其他违反游戏规则类型。

    也许可以使用唯一约束(unique constraint),否则你很容易发生写入偏差。

  • 抢注用户名
  • 唯一约束是一个简单的解决办法多个用户同时注册同一个用户名(第二个事务在提
    交时会因为违反用户名唯一约束而被中止)

  • 防止双重开支
  • 2.4.2 导致写入偏差的幻读:

    幻读:
    一个事务中的写入改变另一个事务的搜索查询的结果。

    物化冲突:
    如果幻读的问题是没有对象可以加锁,也许可以人为地在数据库中引入一个锁对象?
    物化冲突将将幻读变为数据库中一组具体行上的锁冲突

    三、可串行化

    读已提交 、快照隔离、写入偏差、幻读

    3.1 真正的可串行化

    3.1.1 在存储过程中封装事务

    具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库
    如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘 I/O。

    3.1.2 存储过程的优点和缺点

    优点:

    存储过程与内存存储,使得在单个线程上执行所有事务变得可行。由于不需要等待 I/O,且避免了并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。

    3.1.3 分区

    只读事务可以使用快照隔离在其它地方执行,但对于写入吞吐量较高的应用,单线程事务处理器可能成为一个严重的瓶颈。

    为了伸缩至多个 CPU 核心和多个节点,可以对数据进行分区

    事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。
    简单的键值数据通常可以非常容易地进行分区,但是具有多个次级索引的数据可能需要大量的跨分区协调

    3.1.4 串执行小结

  • 每个事务都必须小而快,只要有一个缓慢的事务,就会拖慢所有事务处理。
  • 仅限于活跃数据集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但如果需要在单线程执行的事务中访问,系统就会变得非常慢 。
  • 写入吞吐量必须低到能在单个 CPU 核上处理,如若不然,事务需要能划分至单个分区,且不需要跨分区协调。
  • 跨分区事务是可能的,但是它们能被使用的程度有很大的限制。
  • 如果事务需要访问不在内存中的数据,最好的解决方案可能是中止事务,异步地将数据提取到内存中,同时继续处理其他事务,然后在数据加载完毕时重新启动事务。
    这种方法被称为 反缓存(anti-caching),正如前面在 “在内存中存储一切” 中所述。

    3.2 两阶段锁定

    两阶段锁定(2PL,two-phase locking)
    有时也称为 严格两阶段锁定(SS2PL, strong strict two-phase locking),以便和其他 2PL 变体区分。

    两阶段锁的要求更强:
    只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要 独占访问权限。(读阻塞写,写阻塞读)

    2PL与快照隔离区别:

  • 快照隔离读不阻塞写,写也不阻塞读。2PL读阻塞写,写阻塞读。
  • 2PL 提供了可串行化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏差。快照隔离不可以。
  • 3.2.1 实现两阶段锁

    两阶段锁:
    第一阶段(当事务正在执行时)获取锁,第二阶段(在事务结束时)释放所有的锁。

    2PL 用于 MySQL(InnoDB)和 SQL Server 中的可串行化隔离级别,以及 DB2 中的可重复读隔离级别。
    读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于 共享模式独占模式

    规则如下:

  • 已经有共享锁的条件下,可以继续使用共享锁,但是不能使用排他锁。多个共享锁可以同时使用。
  • 有一个排他锁在使用的情况下,其他的锁都要等待(无论共享锁还是排他锁)
  • 排他锁使用之前如果还有其他的锁(无论是共享锁还是排他锁),则要等待。
  • 因此会引发死锁的情况。

    3.2.2 两阶段锁的性能

    2PL的条件下,事务吞吐量与查询响应时间要比弱隔离级别下要差得多

    开销:

  • 获取和释放所有这些锁的开销
  • 并发性降低,不稳定的延迟。
  • 基于锁实现的读已提交隔离级别可能发生死锁,但在基于 2PL 实现的可串行化隔离级别中,它们会出现的频繁死锁、终止、重试。
  • 3.2.3 谓词锁

    谓词锁不属于特定的对象(例如,表中的一行),它属于所有符合某些搜索条件的对象

  • 如果事务 A 想要读取匹配某些条件的对象,就像在这个 SELECT 查询中那样,它必须获取查询条件上的 **共享谓词锁**(shared-mode predicate lock)。如果另一个事务 B 持有任何满足这一查询条件对象的**排它锁**,那么 A 必须等到 B 释放它的锁之后才允许进行查询。
  • 如果事务 A 想要插入,更新或删除任何对象,则必须首先**检查**旧值或新值是否与任何现有的**谓词锁匹配**。如果事务 B 持有匹配的谓词锁,那么 A 必须等到 B 已经提交或中止后才能继续。
  • 关键:
    谓词锁甚至适用于数据库中尚不存在,但将来可能会添加的对象(幻象)。如果两阶段锁定包含谓词锁,则数据库将阻止所有形式的写入偏差和其他竞争条件,因此其隔离实现了可串行化。

    3.2.4 索引范围锁

    谓词锁的性能不佳: 如果活跃事务持有很多锁,检查匹配的锁会非常耗时
    因此,大多数使用 2PL 的数据库实际上实现了索引范围锁(index-range locking,也称为 next-key locking),这是一个简化的近似版谓词锁

    通过使谓词匹配到一个更大的集合来简化谓词锁是安全的。这种方法能够有效防止幻读和写入偏差。索引范围锁并不像谓词锁那样精确,但开销较低,所以是一个很好的折衷。

    3.3 可串行化的快照隔离

    一方面,我们实现了性能不好(2PL)或者伸缩性不好(串行执行)的可串行化隔离级别。另一方面,我们有性能良好的弱隔离级别,但容易出现各种竞争条件(丢失更新,写入偏差,幻读等)。

    串行化的隔离级别和高性能是从根本上相互矛盾的吗?
    也许不是,可串行化快照隔离(SSI, serializable snapshot isolation) 提供了完整的可串行化隔离级别,但与快照隔离相比只有很小的性能损失。

    3.3.1 悲观与乐观的并发控制

    串行执行可以称为悲观到了极致,,串行化快照隔离 是一种 乐观(optimistic) 的并发控制技术。乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。

    当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可串行化的事务才被允许提交

    如果存在很多 争用(contention,即很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止

    但是,如果有足够的备用容量,并且事务之间的争用不太高,乐观的并发控制技术较好。

    可串行化的快照隔离(SSI):
    事务中的所有读取都是来自数据库的一致性快照,在快照隔离的基础上,SSI 添加了一种算法来检测写入之间的串行化冲突,并确定要中止哪些事务。

    3.3.2 基于过时前提的决策

    先前讨论了快照隔离中的写入偏差,在快照隔离的情况下,原始查询的结果在事务提交时可能不是最新的,因为数据可能在同一时间被修改。

    事务中的查询与写入可能存在因果依赖。为了提供可串行化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。

    数据库如何知道查询结果是否可能已经改变?

  • 检测对旧 MVCC 对象版本的读取(**读之前存在未提交的写入**)
  • 检测影响先前读取的写入(**读之后发生写入**)
  • 3.3.3 检测旧MVCC读取(读之前存在为提交的写入)

    当一个事务从 MVCC 数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。

    为了防止这种异常,数据库需要跟踪一个事务由于 MVCC 可见性规则而忽略另一个事务的写入。当事务想要提交时,数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样,事务必须中止。

    为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务 ?因为如果事务是只读事务,则不需要中止,因为没有写入偏差的风险。通过避免不必要的中止,SSI 保留了快照隔离从一致快照中长时间读取的能力。

    3.3.4 检测影响之前读取的写入(读之后发生写入)

    当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务指导其他读事务完成,而是像警戒线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。

    3.3.5 SSI的性能

    与两阶段锁定相比,SSI 的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁(写不会阻塞读,读不会阻塞写)

    这种设计原则使得: 查询延迟更可预测,变量更少。特别是,只读查询可以运行在一致快照上,而不需要任何锁定,有利于读取繁重的负载。

    中止率显著影响 SSI 的整体表现。例如,长时间的读写事务很可能会发生冲突并中止,因此 SSI 要求同时读写的事务尽量短****(只读的长事务可能没问题)。对于慢事务,SSI 可能比两阶段锁定或串行执行更不敏感。


    总结

    1. 简介

    事务是一个抽象层,允许应用程序假装某些并发问题和某些类型的硬件和软件故障不存在。各式各样的错误被简化为一种简单情况:事务中止(transaction abort),而应用需要的仅仅是重试
    如果没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。

    2. 并发控制——>隔离级别——>竞争条件的例子:

  • 脏读
  • 一个客户端读取到另一个客户端尚未提交的写入读已提交 或更强的隔离级别可以防止脏读。

  • 脏写
  • 一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏写

  • 读取偏差(不可重复读)
  • 同一个事务中,客户端在不同的时间点会看见数据库的不同状态。快照隔离 经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用 多版本并发控制(MVCC) 来实现。

  • 更新丢失
  • 两个客户端同时执行 读取 - 修改 - 写入序列。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(SELECT FOR UPDATE)

  • 写偏差
  • 一个事务读取一些东西,根据它所看到的值作出决定,并将该决定写入数据库。但是,写入时,该决定的前提不再是真实的。只有可串行化的隔离才能防止这种异常。

  • 幻读
  • 事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。快照隔离可以防止直接的幻像读取,但是写入偏差上下文中的幻读需要特殊处理,例如索引范围锁定。

    弱隔离级别可以防止其中一些异常情况,但要求应用程序开发人员手动处理剩余那些(例如,使用显式锁定)。只有**可串行化的隔离才能防范所有这些问题。**我们讨论了实现可串行化事务的三种不同方法:

  • 字面意义上的串行执行
  • 如果每个事务的执行速度非常快,并且事务吞吐量足够低,足以在单个 CPU 核上处理,这是一个简单而有效的选择。

  • 两阶段锁定(2PL)
  • 数十年来,两阶段锁定一直是实现可串行化的标准方式,但是许多应用出于性能问题的考虑避免使用它。

  • 可串行化快照隔离(SSI)
  • 它使用乐观的方法,允许事务执行而无需阻塞。当一个事务想要提交时,它会进行检查,如果执行不可串行化,事务就会被中止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值