跳至主要內容

Chapter14 Transactions

Chiichen原创大约 13 分钟课程笔记数据库

简介

  • 事务(Transaction)是程序执行的一个单位,它访问并可能更新各种数据项。
  • 例如,从账户 A 转账 50 美元到账户 B 的事务:
    1. 读取(A)
    2. A := A - 50
    3. 写入(A)
    4. 读取(B)
    5. B := B + 50
    6. 写入(B)
  • 处理的两个主要问题是:
    • 各种类型的故障,如硬件故障和系统崩溃。
    • 多个事务的并发执行。

资金转账示例

  • 例如,从账户 A 转账 50 美元到账户 B 的事务:
    1. 读取(A)
    2. A := A - 50
    3. 写入(A)
    4. 读取(B)
    5. B := B + 50
    6. 写入(B)
  • 事务的原子性(Atomicity)要求:
    • 如果事务在第 3 步之后、第 6 步之前失败,资金将会“丢失”,导致数据库状态不一致。
    • 失败可能是由软件或硬件引起的。
    • 系统应确保部分执行的事务的更新不会反映在数据库中。
  • 持久性(Durability)要求:
    • 一旦用户被通知事务已完成(即进行了 50 美元的转账),即使出现软件或硬件故障,事务对数据库的更新也必须持久存在。
  • 一致性(Consistency)要求:
    • A 和 B 的总和在事务执行过程中不变。
    • 一般而言,一致性要求包括:
      • 显式指定的完整性约束,如主键和外键。
      • 隐式完整性约束,例如所有账户余额的总和减去贷款金额的总和必须等于手头现金的价值。
    • 一个事务必须看到一个一致的数据库。
    • 在事务执行期间,数据库可能暂时不一致。
    • 当事务成功完成时,数据库必须保持一致。
    • 错误的事务逻辑可能导致不一致性。
  • 隔离性(Isolation)要求 :
    • 如果在步骤 3 和 6 之间,允许另一个事务 T2 访问部分更新的数据库,它将看到不一致的数据库(A + B 之和将小于应有的值)。
      Alt text
    • 通过串行运行事务(即一个接一个)可以轻松确保隔离。
    • 然而,同时执行多个事务有显着的好处,我们稍后会看到。

ACID 属性

  • 事务是一种程序执行的单元,用于访问并可能更新各种数据项。为了保持数据的完整性,数据库系统必须确保以下内容:
    • 原子性(Atomicity):事务的所有操作要么全部正确地反映在数据库中,要么完全不反映在数据库中。
    • 一致性(Consistency):在孤立执行的情况下,事务的执行保持数据库的一致性。
    • 隔离性(Isolation):尽管多个事务可以并发执行,但每个事务必须对其他并发执行的事务毫不知情。中间事务结果必须对其他并发执行的事务隐藏起来。换句话说,对于每对事务 T 和 F,T 看起来要么在 F 完成后开始执行,要么在 T 完成后 F 开始执行。
    • 持久性(Durability):在事务成功完成后,它对数据库所做的更改必须持久存在,即使出现系统故障也是如此。

事务状态(Transaction State)

  • 活动(Active):初始状态,在事务执行期间保持该状态。
  • 部分提交(Partially committed):在执行最后一个语句后。
  • 失败(Failed):在发现无法继续进行正常执行时。
  • 中止(Aborted):在事务被回滚并将数据库恢复到事务开始之前的状态之后。中止后有两个选择:
    • 重新启动事务:仅当没有内部逻辑错误时才能做到。
    • 终止事务。
  • 已提交(Committed):成功完成后。当事务的提交日志记录被输出到稳定存储后,可以说事务已提交。
    事务状态(Transaction State)

并发执行

  • 系统允许多个事务同时运行。其中的优点包括:
    • 提高处理器和磁盘利用率,从而提高事务吞吐量。例如,一个事务可以使用 CPU,而另一个事务可以从磁盘读取或写入数据。
    • 减少事务的平均响应时间:短事务无需等待长事务完成。
  • 并发控制机制:用于实现隔离性和一致性。
    • 即控制并发事务之间的交互,以防止它们破坏数据库的一致性。
    • 在第 16 章中学习并发执行的正确性概念后,将研究并发控制方案。

调度(Schedule)

  • 调度(Schedule)是指定并发事务指令按照时间顺序执行的序列。

    • 对于一组事务的调度,必须包含这些事务的所有指令。
    • 调度必须保持每个事务内指令出现的顺序。
    • 例如,T1={a1,a2,,an}T2={b1,b2,,bn},调度就是要合理执行完{a1,a2,,an,b1,b2,,bn}
  • 成功完成执行的事务将在最后一个语句中包含提交(commit)指令。

  • 默认情况下,假设事务在最后一步执行提交指令。

  • 执行未成功完成的事务将在最后一个语句中包含中止(abort)指令。
    调度(Schedule)- 1
    调度(Schedule)- 2
    调度(Schedule)- 3
    调度(Schedule)- 4

可串行化性(Serializability)

  • 基本假设:每个事务保持数据库的一致性。
  • 因此,一组事务的串行执行保持数据库的一致性。
  • 如果一个(可能并发的)调度等价于一个串行调度,则该调度具有可串行化性。不同形式的调度等价性导致了冲突可串行化(Conflict Serializability)的概念。

冲突指令

  • 我们忽略除读和写指令之外的操作。

  • 我们假设事务可以在读和写之间对本地缓冲区中的数据执行任意计算。

  • 我们的简化时间表仅包含读取和写入指令。

  • 事务TF的指令tf冲突的条件是:

    1. 存在某个项目Qtf同时访问。
    2. 这些指令中至少有一条对Q进行了写操作。
  • 以下是一些例子来说明冲突与否:

    1. t=read(Q)f=read(Q)tf不冲突。
    2. t=read(Q)f=write(Q)。它们冲突。
    3. t=write(Q)f=read(Q)。它们冲突。
    4. t=write(Q)f=write(Q)。它们冲突。
  • 为什么可以调度?

    • 如果tf在一个调度中是连续的,并且它们之间没有冲突,那么即使它们在调度中被交换位置,它们的结果仍然保持不变。

冲突可串化(Conflict Serializability)

  • 如果一个调度S可以通过一系列非冲突指令的交换转变为调度S2,我们称SS2在冲突方面是等价的。我们称调度S是冲突可串行化的,当且仅当它与一个串行调度在冲突方面是等价的。

  • 换句话说,调度S是冲突可串行化的,当且仅当通过交换调度S中非冲突指令的顺序,我们可以得到一个新的调度S2,并且S2是一个串行调度。

  • 下图所示的时间表 3 可以通过一系列不冲突指令的交换转换为时间表 6,这是一个串行时间表,其中 T2 跟随 T1。因此 Schedule 3 是可冲突串行化的。
    冲突可串化(Conflict Serializability) - 1

  • 而下图就不行
    冲突可串化(Conflict Serializability) - 2

检验串行性(Testing for Serializability)

  • 已知某个调度方案,我们怎么知道它是不是串行的
  • 考虑一组事务T1T2Tn的某个调度。
  • 先行图(Precedence graph)是一个有向图,其中顶点是事务(名称)。
  • 如果满足以下三个条件之一,我们在 T1 和 T2 之间绘制一条弧T1T2
    1. T1T2执行read(Q)之前执行write(Q)
    2. T1T2执行write(Q)之前执行read(Q)
    3. T1T2执行write(Q)之前执行write(Q)
  • 我们可以用被访问的项目标记弧。
  • 判断定理:如果一个调度的先行图是无环的,则该调度是冲突可串行化的。
    先行图(Precedence graph)
  • 可以通过对这个有向图进行拓扑排序来检测是否无环O(n+e),并且拓扑排序的结果就是一个可能的串行运行方案

可恢复调度(Recoverable Schedules)

  • 需要解决事务失败对同时运行的事务的影响。
  • 可恢复调度(Recoverable schedule):
    • 已知T:R(Q)F:W(Q)在项目Q上冲突。
    • 如果在某个调度中,先执行FW(Q),后执行TR(Q),先提交F后提交T,那么该调度是可恢复的。
      可恢复调度(Recoverable Schedules)
  • 以上调度,如果 T9 在读操作之后立即提交,则不可恢复。
  • 如果 T8 应该中止,那么 T9 将读取(并可能显示给用户)一个不一致(inconsistent)的数据库状态。因此,数据库必须确保调度是可恢复的。

级联回滚(Cascading Rollbacks)

  • 级联回滚——单个事务失败会导致一系列事务回滚。考虑以下计划,其中尚未提交任何事务(因此该计划是可恢复的)
Schedule
Schedule
  • 如果T8失败,T9T10也必须回滚。
  • 级联回滚的目的是为了获得一致性。
  • 可能导致大量工作白费

无级联调度(Cascadeless Schedules)

  • 无级联调度——不会发生级联回滚;对于每对发生冲突的事务{T,F}先执行FW(Q),并且先提交F,后执行TR(Q)并且后提交
  • 每个无级联调度也是可恢复的
  • 最好将调度限制为那些无级联的调度

并发控制(Concurrency Control)

  • 为了保持数据库的一致性,调度必须满足以下条件:
    1. 冲突可串行化或视图可串行化。
    2. 可恢复。
    3. 最好是无级联的。
  • 只允许一次执行一个事务的策略会生成串行调度,但提供了较低的并发度。
  • 并发控制方案在允许的并发度和产生的开销之间做出权衡。
  • 有些方案只允许生成冲突可串行化的调度,而其他方案则允许生成既不冲突可串行化也视图可串行化的调度。

并发控制与可串行性测试的比较

  • 并发控制协议允许并发调度,但确保调度同时满足以下条件:
    1. 冲突可串行化/视图可串行化(保证隔离性)。
    2. 可恢复(保证一致性)。
    3. 无级联(降低中止时的开销)。
  • 并发控制协议通常不会在创建先行图时进行检查。
  • 相反,协议会施加一种规则,避免非可串行化的调度。
  • 我们在第 16 章中学习此类协议。

弱一致性级别(Weak Levels of Consistency)

  • 有些应用程序可以接受较弱的一致性级别,允许不可串行化的调度。
  • 例如,一个只读事务想要获取所有账户的近似总余额。
  • 例如,为了查询优化而计算的数据库统计信息可以是近似的(为什么?)。
  • 这类事务不需要与其他事务可串行化。
  • 在准确性和性能之间进行权衡。

SQL-92 中的隔离级别

  • Serializable(可串行化)- 默认级别。
  • Repeatable read(可重复读取)- 只能读取已提交的记录,重复读取同一记录必须返回相同的值。然而,一个事务可能不是可串行化的 - 它可能找到一些被事务插入的记录,但找不到其他记录。
  • Read committed(读已提交)- 只能读取已提交的记录,但连续的对同一记录的读取可能返回不同的(但已提交的)值。
  • Read uncommitted(读未提交)- 可以读取未提交的记录。
  • 较低级别的一致性对于收集关于数据库的近似信息很有用。
  • 警告:一些数据库系统默认情况下不能确保可串行化的调度。
  • 例如,Oracle 和 PostgreSQL 默认支持称为快照隔离的一致性级别(不是 SQL 标准的一部分)。

并发导致的数据读取问题

  • 脏读(读取未提交数据)
    • A 事务读取 B 事务尚未提交的数据,此时如果 B 事务发生错误并执行回滚操作,那么 A 事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于 B 事务更改了它,这个数据变得不再纯粹。这个时候 A 事务立即读取了这个脏数据,但事务 B 良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务 A 却什么都不知道,最终结果就是事务 A 读取了此次的脏数据,称为脏读。
  • 不可重复读(前后多次读取,数据内容不一致)
    • 事务 A 在执行读取操作,由整个事务 A 比较大,前后读取同一条数据需要经历很长的时间 。而在事务 A 第一次读取数据,比如此时读取了小明的年龄为 20 岁,事务 B 执行更改操作,将小明的年龄更改为 30 岁,此时事务 A 第二次读取到小明的年龄时,发现其年龄是 30 岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。
  • 幻读(前后多次读取,数据总量不一致)
    • 事务 A 在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务 B 执行了新增数据的操作并提交后,这个时候事务 A 读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,称为幻读。
  • 不可重复读和幻读的区别:
    • 不可重复读是读取了其他事务更改的数据,针对 update 操作
    • 解决:使用行级锁,锁定该行,事务 A 多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
    • 幻读是读取了其他事务新增的数据,针对 insert 和 delete 操作
    • 解决:使用表级锁,锁定整张表,事务 A 多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

SQL 中的事务定义

  • 数据操作语言必须包括一种构造,用于指定组成事务的一系列操作。
  • 在 SQL 中,事务隐式地开始。
  • SQL 中的事务通过以下方式结束:
    • Commit work:提交当前事务并开始新事务。
    • Rollback work:导致当前事务中止。
  • 在几乎所有数据库系统中,默认情况下,如果 SQL 语句执行成功,每个 SQL 语句也会隐式提交。
  • 可以通过数据库指令关闭隐式提交。
    • 例如,在 JDBC 中,可以使用connection.setAutoCommit(false)来关闭隐式提交。