文章目录

  • 1. 事务为什么这么重要
    • 1.1 事务的概念
    • 1.2 需要事务的原因
  • 2. 事务的棘手概念
    • 2.1 ACID 的含义
    • 2.3 单对象和多对象操作
      • 2.3.1 单对象写入
      • 2.3.2 多对象事务的需求
      • 2.3.3 处理错误和中止
  • 3. 弱隔离级别
    • 3.1 读已提交
    • 3.2 实现读已提交
    • 3.3 快照隔离和可重复读
      • 3.3.1 实现快照隔离
      • 3.3.2 观察一致性快照的可见性规则
      • 3.3.3 索引和快照隔离
      • 3.3.4 可重复读与命名混淆
    • 3.4 防止丢失更新
      • 3.4.1 原子写
      • 3.4.2 显示锁定
      • 3.4.3 自动检测丢失的更新
      • 3.4.4 比较并设置(CAS)
      • 3.4.5 冲突解决和复制
      • 3.4.5 写入偏差的特征
      • 3.4.6 导致写入偏差的幻读
      • 3.4.7 物化冲突
  • 4. 可序列化
    • 4.1 真的串行执行
      • 4.1.1 在存储过程中封装事务
      • 4.1.2 分区
      • 4.1.3 串行小结
    • 4.2 两阶段锁定(2PL)
      • 4.2.1 实现两阶段锁
      • 4.2.2 两阶段锁定的性能
      • 4.2.3 谓词锁
      • 4.2.4 索引范围锁
    • 4.3 序列化快照隔离
      • 4.3.1 悲观和乐观的并发控制
      • 4.3.2 可序列化的快照隔离的性能
  • 5. 写在最后

1. 事务为什么这么重要

1.1 事务的概念

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

注:事务不是天然存在的,它们是为了简化应用编程模型而创建的。通过使用事务,应用程序可以忽略潜在的错误情况和并发问题,因为数据库会替应用处理好这些。

1.2 需要事务的原因

在数据的使用中,现实是残酷的,很多事情都可能出错:

  • 数据库软件、硬件可能在任意时刻发生故障,比如写操作进行到一半的时候

  • 应用程序可能在任意时刻崩溃(包括一系列操作的中间

  • 网络中断可能会意外切断数据库与应用的连接,或数据库之间的连接

  • 多个客户端可能会同时写入数据库,覆盖彼此的更改

  • 客户端可能读取到无意义的数据,因为数据只更新了一部分

  • 客户之间的竞争条件可能导致令人惊讶的错误

  • ……

为了实现可靠性,系统必须处理这些故障,确保它们不会导致整个系统灾难性故障。所以事务就应运而生。它简化应用编程模型。通过事务,应用程序可以自由地忽略某些潜在的错误和并发问题。

注:为了获取更高的性能或高可用性,并不是所有的应用都需要事务,有的时候需要弱化事务保证,或完全放弃事务。

2. 事务的棘手概念

如今,人们普遍认为事务是可伸缩性的对立面,任何大型系统都必须放弃事务以保持良好的性能和高可用性。而另一方面,数据库厂商有时将事务保证作为「重要应用」和「有价值数据」的基本要求。这两种观点都是纯粹的夸张。

事实并非如此简单:与其他技术设计选择一样,事务有其优势和局限性。

2.1 ACID 的含义

ACID 代表原子性(Atomicity)、一致性(Consistency)、隔离性(lsolation)和持久性(Duration)。

注:不符合 ACID 的标准成为 BASE,它代表基本可用性(Basically Available)、软状态(Soft State)和最终一致性(Eventualconsistency),似乎 BASE 的唯一合理的定义是「不是ACID」,毕竟它几乎可以代表任何你想要的东西

**原子性(Atomicity):**原子是指不能分解成更小部分的东西。特征是:能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。

一致性(Consistency): 对数据的一组特定约束必须始终成立。即不变量,在会计系统中,所有账户整体必须借贷相抵。一致性的这种概念取决于应用程序对不变量的观念,应用程序负责正确定义它的事务,并保持一致性。

注:如果一个事务开始于一个满足上述不变量的有效数据库,且事务处理期间的任何写入都保持这种有效性,那么可以确定,不变量总是满足的。

原子性,隔离性和持久性是数据库的属性,而一致性是应用程序的属性

隔离性(Isolation): 同时执行的事务是互相隔离的,它们不能互相影响。

注:在实际隔离性使用中通常使用一种快照隔离的功能,这是一种比可序列化更弱的保证。

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

注:完美的持久性是不存在的,在实践中,没有一种技术可以提供绝对的保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份——它们可以且应该一起使用。与往常一样,最好抱着怀疑的态度接受任何理论上的「保证」。

2.3 单对象和多对象操作

2.3.1 单对象写入

当单个对象发生改变时,原子性和隔离性也是适用的。思考以下问题:假设存储一个长度为 20 KB的 JSON 文档:

  • 如果在发送第一个 10KB 之后网络连接中断,数据库是否存储了不可解析的 10KB JSON 片段?
  • 如果数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起?
  • 如果另一个客户端在写入过程中读取该文档,是否看到这部分更新的值?

这些问题非常让人头大,故存储引擎一个普遍的目标是:对单节点的单个对象(例如键值对对象)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复,并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象)

注:单对象的操作很有用,它可以防止多个客户端尝试同时写入同一个对象时丢失更新。但是它不是通常意义上的事务,事务通常被理解为,将多个对象上的多个操作合并为一个执行单元的机制。

2.3.2 多对象事务的需求

许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性和高性能的情况下,它们可能会碍事。

我们是否需要多对象事务?答案是肯定的,因为在许多场景下我们都需要协调写入几个不同的对象,比如:

  • 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。支持多对象事务是你确保这些引用始终有效。
  • 在具有二级索引的数据库中,每次更改值时都需要更新索引。从事务的角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离性,记录可能出现在一个索引中,但是没有出现在另一个索引中个,因为第二个索引的更新还没有发生。

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

2.3.3 处理错误和中止

「事务的一个关键特性是,如果发生错误,它可以中止并安全地重试。」

  • ACID 数据库基于这样的哲学:如果数据库有违反原子性,隔离性或持久性的危险,则宁愿完全放弃事务,而不是留下半成品
  • 然而不是所有的系统都遵循上述事务的哲学,很多数据库在运行遇到错误的时候,不会撤销,所以,从错误中恢复是应用程序的责任

注:错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况,而不是处理复杂错误。这个错误通常会导致一个堆栈向上的传播的异常,这实在太不友好了,因为中止的重点就是允许安全的重试

中止的事务是一个简单而有效的错误处理机制,但是在以下条件下使用它并不是一个明智的选择:

  • 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障,那么重试事务会导致事务被执行两次(ps 此处需要应用有一个额外的去重机制
  • 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,那么可以限制重试次数,比如使用指数退避算法。
  • 仅在临时性错误(例如:死锁,异常情况,临时性网络中断和故障切换)后才值得重试。在发生永久性错误(例如:违反约束)之后重试是毫无意义的
  • 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生副作用。例如,如果正在发送电子邮件,那么肯定不希望每次重试事务时都重新发送电子邮件。建议使用「两阶段提交」确保几个不同的系统一起提交或放弃。

3. 弱隔离级别

数据库一直试图通过提供事务隔离来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发的发生,让 code 更加简单。

注:可序列化的隔离级别意味着数据库保证事务的效果如同连续运行(即一次一个,没有任何并发)。但是可序列化会有性能损失,很多数据库不愿意支付这个代价。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发的问题。

注:未必需要百分之百的解决问题,可能只需要或只能够百分之九十的解决问题,意识到这点很重要。

3.1 读已提交

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

  • 从数据库读时,只能看到已提交的数据(没有脏读)

    • 所谓脏读,就是一个事务将一些数据写入数据库,且还没有提交或中止,另一个事务能看到未提交的数据
  • 写入数据库是,只会覆盖已经写入的数据(没有脏写)
    • 所谓脏写,是指如果两个事务同时尝试更新数据中的相同对象,先前写入的尚未提价事务的一部分,被后面写入会覆盖一个尚未提交的值

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

3.2 实现读已提交

读已提交是一个非常常用的隔离级别,这是 Oracle 11g,PostgreSQL,SQL Server 2012 和其他许多数据中的默认设置。

最常见的情况是:

  • 使用行锁来防止脏写:当事务想要修改特定对象(行或者文档)时,它必须首先获得该对象的锁。然后必须持有该锁知道事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能取该锁并继续。
  • 防止脏读,对于写入的每个对象,数据库都会记住旧的已提交的值,和由当前持有写入锁的事务设置的新值。当事务正在进行是,任何其他读取对象的事务都会拿到旧值。只有当新值提交后,事务才会切换到读取新值。

3.3 快照隔离和可重复读

在读已提交的模式下,确实有很多优势,但是有一个显著的缺点,即会产生不可重复读或读取偏差的问题。

  • 初始情况, Alice 账户的两个账户1, 2 分别有 500 美元
  • 在转账之前, Alice 查询到 1 账户有 500 美元
  • 转账人,从 2 账户转账 200 美元到 1 账户
  • Alice 在转账后,查询到 2 账户有 400 美元

此处,账户总计的 1000 美元跟 Alice查询到的 900 美元不符合,即出现了不可重复读的现象

快照隔离是解决上述不可重复读最常见的方法,核心思路是,每个事务都从数据库的一致快照中读取,事务可以看到事务开始时数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。

3.3.1 实现快照隔离

多版本并发控制(MVCC)用于实现快照隔离,数据库必须保留一个对象的几个不同的提交版本,因为正在运行的事务需要看到数据库在不同时间点的状态。

注:读已提交的隔离规则,保存一个对象的两个版本就足够了,提交的版本和被覆盖尚未提交的版本。

下图为 PostgreSQL 实现的基于 MVCC 的快照隔离。当事务开始时,它被赋予一个唯一的,永远增长的事务 ID。每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务 ID。

注:事实上,事务ID 是 32 位整数,所以 40 亿次事务之后溢出。PostgreSQL 有清理旧的事务 ID 的机制,确保事务ID 溢出不会影响到数据。

3.3.2 观察一致性快照的可见性规则

当一个事务从数据库中读取时,事务 ID 用于决定它可以看见哪些对象,看不见哪些对象。具体工作规则如下:

  • 每次事务开始时,数据库列出当时所有尚未提交或中止的事务清单,即使之后提交后,这些事务已经执行的任何写入也会被忽略
  • 被中止事务所执行的任何写入都将被忽略
  • 在当前事务开始之后开始的事务的任何写入都被忽略,而不管这些事务是否已经提交
  • 所有的写入,对应用都是可见的。

总结,如果以下两个条件都成立,则可见一个对象:

  • 读事务开始时,创建该对象的事务已经提交
  • 对象未被标记为删除,或如果被标记为删除,请求删除的事务在读事务开始时尚未提交。

3.3.3 索引和快照隔离

索引如何在多版本数据库中工作?

答:一种选择是使索引简单地指向对象的所有版本,并且需要索引查询来过滤掉当前事务不可见的任何对象版本。当垃圾收集删除任何事务不再可见得旧对象版本时,相应地索引条目也可以被删除。

在实践中,许多实现细节决定了多版本并发控制的性能。比如:

  • 将同一对象的不同版本放入同一个页面中
  • 使用一种写时拷贝的变体,在更新是不覆盖树的页面,而为每个修改页面创建一份副本。
  • ……

3.3.4 可重复读与命名混淆

快照隔离是一个有用的隔离级别,特别是对于只读事务而言。但是,许多数据库实现它,却用不同的名字来称呼。在 Oracle 中称为可序列化的,在 PostgreSQL 和 MySQL 中称为可重复读。其可重复读是通过快照隔离来实现的。

注:不幸的是,SQL 标准对隔离级别的定义是有缺陷的——模糊、不精确,并不像标准应有的样子独立于实现。有几个数据库实现了可重复读,但他们实现提供的保证存在很大的差异,尽管表面上市标准化的。在研究文献中已经有了可重复读的正式定义,但是大多数实现并不能满足这个正式定义,

3.4 防止丢失更新

目前讨论的读已提交和快照隔离级别,主要保证了读事务在并发时能够看到什么。却忽略了两个事务并发写入导致丢失更新的问题。即两个事务同时执行,其中一个事务的修改丢失的问题。比如:

  • 增加计数器或更新账户余额(需要读取当前值,计算新值并写回更新后的值)
  • 两个用户同时编辑 wiki 页面,每个用户通过将整个页面内容发送到服务器来保存更改,覆盖数据库中当前的内容。

这是一个普遍的问题,所以已经开发出来各种解决方案。

3.4.1 原子写

许多数据库提供了原子更新操作,从而消除了在应用程序代码中执行读取——修改——写入序列的需要。

UPDATE counters SET value = value + 1 WHERE key = 'foo';

注:当 key 为 foo 才更新本条记录

3.4.2 显示锁定

如果数据库的内置原子操作没有提供必要的功能,防止丢失更新的另一个选择是让应用程序显示地锁定将要更新的对象。

BEGIN TRANSACTION;
SELECT * FROM figuresWHERE name = 'robot' AND game_id = 222
FOR UPDATE;-- 检查玩家的操作是否有效,然后更新先前SELECT返回棋子的位置。
UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;

注:FOR UPDATE 子句告诉数据库应该对该查询返回的所有行加锁

3.4.3 自动检测丢失的更新

另一种方式是允许并发事务并行执行,如果事务管理器检测到丢失更新,则会中止事务并强制重试。

注:PostgreSQL 的可重复读,Oracle 的可串行化和 SQL Server 的快照隔离级别,都会自动发检测到丢失更新并中止事务,但是, MySQL/InnoDB 的可重复读并不会检测丢失更新。

3.4.4 比较并设置(CAS)

在不提供事务的数据库中,可以使用「比较并设置(CAS)」来实现原子操作。只有当前值从上次读取时一直未改变,才允许更新发生。此操作可以避免丢失更新。

-- 根据数据库的实现情况,这可能也可能不安全
UPDATE wiki_pages SET content = '新内容'WHERE id = 1234 AND content = '旧内容';

注:如果数据库允许 WHERE 子句从旧快照中读取,则此语句可能无法防止丢失更新。

3.4.5 冲突解决和复制

在复制数据库中,防止丢失更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。

  • 允许并发写入创建多个冲突版本的值,并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。

  • 使用最后写入胜利(LWW)解决冲突。

    注: LWW 很容易丢失更新,但是确实很多复制数据库中的默认方案

3.4.5 写入偏差的特征

写入偏差,既不是脏写也不是丢失更新。是指两个事务正在更新不同的对象。此处发生的冲突并不明显,只有在事务并发进行时才有可能。

在上述的两个事务中,首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以完全地休班。由于数据库使用快照隔离,两次检查都返回 2,所以两个事务都进行了下一个阶段。Alice 、 Bob 在竞争条件发生的情况下,两个事务都提交成功了,所以现在没有医生值班了。这违反了至少有一名医生值班的要求。

3.4.6 导致写入偏差的幻读

所有写入偏差都能够抽象为一类模式:

  • 一个 SELECT 查询找出符合条件的行,并检查是否符合一些要求
  • 按照第一个查询的结果,应用代码决定是否继续
  • 如果应用决定继续操作,就执行写入,并提交事务

注:上述模式的两个事务,并发发生则可能会产生写入偏差

一个事务中的写入改变另一个事务的搜索查询结果,称之为幻读。快照隔离避免了只读查询中幻读,但是在读写事务中,幻读会导致写入偏差问题

3.4.7 物化冲突

如果幻读的问题是没有对象可以加锁,则可以认为地在数据库中引入一个锁对象。

例如,在会议室预定的场景中,可以创建一个关于时间槽和房间的表。此表中的每一行对应于特定时间段的特定房间。可以提前插入房间和时间的所有可能组合行。

注:读了很多次,没懂这是啥意思……

这种方法被称为物化冲突,因为它将幻读变为数据库中的一组具体行上的锁冲突。

4. 可序列化

可序列化隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。即数据库可以防止所有可能的竞争条件。

大多数提供可序列化的数据库都使用以下三种技术之一:

  • 字面意义上的串行执行事务

  • 两相锁定(2PL,two-phase locking)

  • 乐观并发控制技术

4.1 真的串行执行

避免并发问题的最简单的方法就是完全不要并发:在单个线程上按顺序一次只执行一个事务。这样做就完全绕开了检测/防止事务冲突的问题,由此产生的隔离,正式可序列化的定义。

如果多线程并发在过去 30 年中被认为是获得良好性能的关键所在,那么究竟是什么改变致使单线程执行变为可能?

答:设计用于单线程执行的系统有时可以比支持并发的系统更好,因为它可以避免锁的协调开销。

4.1.1 在存储过程中封装事务

在数据库的早期阶段,意图是数据库事务可以包含整个用户活动流程。例如,预定机票是一个多阶段的过程(搜索路线,票价和可用作为,决定行程等)。数据库设计者认为,如果整个过程是一个事务,那么它就可以被原子化地执行。

不幸的是,人类做出决定和回应的速度非常缓慢。如果数据库事务需要等待来自用户的输入,则数据库需要支持潜在的大量并发事务,其中大部分是空闲的。

在这种交互式的事务方式中,应用程序和数据库之间的网络通信耗费了大量的时间。如果不允许数据库中进行并发处理,且一次只处理一个事务,则吞吐量将会非常糟糕。

处于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。具体区别如下图所示:

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

4.1.2 分区

顺序执行所有事务并发控制简单多了,但数据库的事务吞吐量被限制为单机单核的速度。只读事务可以使用快照隔离在其它地方执行,但对于写入吞吐量较高的应用,单线程事务处理器可能成为一个严重的瓶颈。

为了伸缩只多个 CPU 核心和多个节点,可以对数据进行分区。但是,对于需要访问多个分区的任意事务,数据库必须在触及的所有分区之间协调事务。由于跨分区事务具有额外的协调开销,所以他们比单分区事务慢很多。

注:存储过程需要跨越所有分区锁定执行,以确保整个系统的可串行性。

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

4.1.3 串行小结

在特定约束条件下,真的串行事务,已经成为一种实现可序列化隔离等级的可行办法:

  • 每个事务必须小而快,只要有一个缓慢的事务,就会拖慢所有事务的处理。
  • 仅限于活跃事务集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但如果需要在单线程执行的事务中访问,系统就会变得非常慢。
  • 写入吞吐量必须低到能在单个 CPU 核上,如若不然,事务需要能划分至单个分区,且不需要跨分区协调
  • 跨分区事务是可能的,但是它们的使用程度有很大的限制。

4.2 两阶段锁定(2PL)

常用的锁通常用于防止脏写:如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。

两阶段锁定与常用的锁类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入,就需要独占访问权限:

  • 如果事务 A 读取一个对象,并且事务 B 想要写入该对象,那么必须等到 A 提交或中止才能继续。(这确保 B 不能再 A 底下意外地改变对象)
  • 如果事务 A 写入一个对象,并且事务 B 想要读取该对象,则 B 必须等到 A 提交或中止才能继续。

注:在 2PL 中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得读不阻塞写,写也不阻塞读

4.2.1 实现两阶段锁

读与写的阻塞是为数据库中每个对象添加锁来实现的。锁可以处于共享模式或独占模式。锁使用如下:

  • 若事务要读取对象,则必须先以共享模式获取锁。允许多个事务同时持有共享锁。但如果另一个事务已经在对象上持有排他锁,则这些事务必须等待。
  • 若事务要写入一个对象,它必须首先以独占模式获取。没有其他事务可以同时持有锁(无论是共享模式还是独占模式),所以如果对象上存在任何锁,该事务必须等待。
  • 如果事务先读取再写入对象,则它可能会将其共享锁升级为独占锁。升级锁的工作方式与直接获得排他锁相同。
  • 事务获得锁之后,必须继续持有锁直到事务结束。这就是「两阶段」这个名字的来源:第一阶段,当事务正在执行时获取锁,第二阶段在事务结束时释放所有的锁。

4.2.2 两阶段锁定的性能

按照设计,如果两个并发事务试图做任何可能导致竞争条件的事件,那么必须等待另一个完成。因此,允许 2PL 的数据库可能具有相当不稳定的延迟,如果工作负载中存在争用,那么可能高百分位点处的响应会非常的慢。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能吧系统的其他部分拖慢,甚至迫使系统停机。

4.2.3 谓词锁

在「导致写入偏差的幻读」中,我们讨论了幻读的问题,即一个事务改变了另一个事务的搜索查询结果。具有可序列化隔离级别的数据库必须防止幻读。

如何实现这一点?

从概念上讲,需要一个谓词锁。它类似于共享/排它锁。但不属于特定的对象。谓词锁的限制访问,如下所示:

  • 如果事务 A 想要读取匹配某些条件的对象,就像在下面的 SELECT 查询中那样,它必须获取查询条件上的共享谓词锁。如果另一个事务 B 持有任何满足这一查询条件的排它锁,那么 A 必须等到 B 释放它的锁之后才允许进行查询。
  • 如果事务 A 想要插入,更新或删除任何对象,则必须首先检查旧值或新值是否与任何现有的为此锁匹配。如果事务 B 持有匹配的谓词锁,那么事务 A 必须等到 B 已经提交或中止之后才能继续。
SELECT * FROM bookings
WHERE room_id = 123 ANDend_time > '2018-01-01 12:00' AND start_time < '2018-01-01 13:00';

4.2.4 索引范围锁

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

「索引范围锁」能够有效防止幻读和写入偏差。索引范围锁并不像谓词锁那么精确(它们可能会锁定更大范围的对象,而不是维持可串行化必须的范围),但是由于此种方式的开销较低,所以是一个很好的折中。

4.3 序列化快照隔离

上述描绘了数据库中并发控制的黯淡画面。一方面,我们实现了性能不好(2PL)或伸缩性不好(串行执行)的可序列化隔离级别。另一方面,我们有性能良好的弱隔离级别,但容易出现各种竞争条件(丢失更新,写入偏差,幻读等)。序列化的隔离级别和高性能是从根本上互相矛盾的吗?

答案:也许不是,一个称为可序列化快照隔离的算法是非常有前途的。它提供了完整的可序列化隔离级别但与快照隔离相比只有很小的性能损失。

注:可序列化快照隔离用于单点数据库(PostgreSQL9.1 之后的可序列化隔离级别)和分布式数据库(FoundationDB)

4.3.1 悲观和乐观的并发控制

两阶段锁是一种所谓的悲观并发控制机制:它基于这样的原则:如果有数据可能出错,最好等到情况安全后再做任何事情。这就像互斥,用于保护多线程编程中的数据结构。

从某种意义上说,串行执行可以被称为悲观到极致:在事务持续期间,每个事务对整个数据库具有排它锁,作为对悲观的补偿,我们让每笔事务执行得非常快,所以只需要短时间持有「锁」。

相比之下,序列化快照隔离是一种乐观的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不组织事务,而是继续执行事务,希望一切会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生;如果是的话,事务将被中止,并且必须重试。只有可序列化的事务才允许提交。

顾名思义,序列化快照隔离——也就是说,事务中的所有读取都来数据库的一致性快照。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,序列化快照隔离添加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。

4.3.2 可序列化的快照隔离的性能

许多工程细节都会影响算法的实际表现。例如一个权衡是跟事务的读取和写入的粒度。如果数据库详细地跟踪每个事务的活动,那么可以准确地确定哪些事务需要中止,但是记录的开销变得显著。

与两阶段锁定相比,可序列化快照隔离的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁。这种设计原则使得查询延迟更可预测,变量更少,特别是,只查询可以运行在一致性快照上,不需要任何锁定,这对于读取繁重的工作负载非常有吸引力。

5. 写在最后

接受人间宝藏小姐姐投喂的一天。即使在总结,也觉得很暖。

  • 我们对缺失的很敏感,对拥有的很迟钝,所以我们总是得不偿失。

  • 夏天就是要温柔,热烈,闪闪发光,所有热爱的事物都要不遗余力。盛夏白瓷梅子汤,碎冰撞壁叮当响

教育的目的是让学生们摆脱现实的奴役,而现在的年轻人正意图做着相反的努力,为了适应现实而改变自己。

有被这句话惊艳到。

设计数据密集型应用——事务(7)相关推荐

  1. 设计数据密集型应用程序_设计数据密集型应用程序书评

    设计数据密集型应用程序 Realising how little you know about something can potentially be a demoralising experien ...

  2. 设计数据密集型应用 第五章:复制

    设计数据密集型应用 第五章:复制 与可能出错的东西比,'不可能'出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了. --道格拉斯·亚当斯(1992) 文章目录 设计数据密集型应用 第五章:复 ...

  3. 设计数据密集型应用 第四章:编码与演化

    设计数据密集型应用 第四章:编码与演化 唯变所适 --以弗所的赫拉克利特,为柏拉图所引(公元前360年) 文章目录 设计数据密集型应用 第四章:编码与演化 编码数据的格式 术语冲突 语言特定的格式 J ...

  4. 设计数据密集型应用 第三章:存储与检索

    3. 第三章:存储与检索 建立秩序,省却搜索 --德国谚语 文章目录 3. 第三章:存储与检索 驱动数据库的数据结构 哈希索引 SSTables和LSM树 构建和维护SSTables 用SSTable ...

  5. 设计数据密集型应用—— 数据系统的未来(12 下)

    文章目录 1. 写在最前面 2. 将事情做正确 2.1 数据库的端到端原则 2.1.1 正好执行一次操作 2.1.2 抑制重复 2.1.3 操作标识符 2.1.4 端到端的原则 2.1.5 在数据系统 ...

  6. 设计数据密集型应用 第六章:分区

    6. 分区 我们必须跳出电脑指令序列的窠臼. 叙述定义.描述元数据.梳理关系,而不是编写过程. -- Grace Murray Hopper,未来的计算机及其管理(1962) 文章目录 6. 分区 术 ...

  7. 设计数据密集型应用-C5-主从架构及同步延迟问题

    本文是<设计数据密集型应用>第5章学习笔记. 什么是Replication Replication是在多台机器上维护的相同的数据,即副本.保存副本的原因有以下几种: 减小延迟:使得地理位置 ...

  8. 设计数据密集型应用(一),DDIA

    零.开篇词 欢迎各位,给大家分享一下自己最近读的书,ddia ,全名叫做:design data-intensive application 直译为:设计数据密集型应用:或者叫做,数据密集型应用的设计

  9. 设计数据密集型应用 第二章:数据模型与查询语言

    第二章:数据模型与查询语言 语言的边界就是思想的边界. -- 路德维奇·维特根斯坦,<逻辑哲学>(1922) 文章目录 第二章:数据模型与查询语言 关系模型与文档模型 NoSQL的诞生 对 ...

最新文章

  1. 解读比特币白皮书:点对点电子现金系统
  2. php移动代码,复制移动文件 - PHP
  3. UMDF驱动开发入门
  4. python 画图 线标注_Python画图的这几种方法,你学会了吗
  5. P3293-[SCOI2016]美味【主席树】
  6. 正则表达式:获取一串字符串中,某个字符串到某个字符串之间的字符串,不包含左右,只取中间
  7. java restsharp_C# RestSharp应用
  8. 基于CUDA的粒子系统的实现
  9. Could not initialize class com.jacob.activeX.ActiveXComponent
  10. 雷军在线求饶:小米5G手机价格厚道,求别骂、求好评、求别带节奏
  11. 互联网的逻辑和电商的逻辑是不一样的
  12. python︱写markdown一样写网页,代码快速生成web工具:streamlit 数据探索案例(六)
  13. 康佳电视手机遥控器android版,康佳电视遥控器
  14. 基于核的黎曼编码和字典学习
  15. 上一步,下一步(撤销和恢复)
  16. linux 操作excel文件,Linux下输出excel文件
  17. 【C语言】浮点型数据在内存中的存储方式
  18. 计算机网络socket翻译成中文,Socket的错误码和描述(中英文翻译)
  19. [CocosCreator]热更新插件使用心得以及注意事项
  20. 有关计算机病毒的说法中正确的是,以下有关计算机病毒的说法中,正确的是()。A.计算机病毒是一些人为编制的程序B.计算机病毒具有隐蔽...

热门文章

  1. 悟空问答 App 宣布下线 品牌启用双拼域名wukong.com
  2. 中国联通排名第四,2019年云综合收入220亿元
  3. MySQL之undo log
  4. 简单实现获取短信验证码倒计时效果
  5. 山石hcsa认证考试内容_山石防火墙HCSA认证视频教程 理论+实验细致讲解 17集非常难得的 山石防火墙视频教程...
  6. “用户密码”形同虚设,“多因素认证”势在必行
  7. ANSYS Maxwell 2D螺线管磁场分析
  8. mysql char转int_在sql语句中怎样把char类型转换成int类型?
  9. 图形界面介绍Attribute Editor
  10. 虚拟机服务器改完端口要重启吗,VMware ESXi中安装虚拟机后怎么改端口号