跳到主要内容

数据库事务(InnoDB ACID)

用语
解释
数据库存取数据的系统。在当前文章中,泛指关系型数据库
查询泛指针对数据记录而执行的操作(,而非特指select)。
逻辑单元在功能或语义上紧密相关、共同完成某个特定任务的一组代指令。
数据完整性在数据的整个生命周期内,维护和保证其准确性一致性
数据库对象databasetableviewindexprocedurefunctiontrigger 这些。
表空间保存数据库对象底层实际数据或结构的存储位置。
MVCC全称是多版本并发控制。一种用于提高事务并发访问性能的机制。
ACIDatomicity, consistency, isolation, durability 的简称。
不变性泛指在某种生命周期内,固定不变(恒成立)的规则。
并发进程之间拥有共享状态,但互相无法感知。
用例系统接收外部请求(例如:用户输入)并做出响应的潜在场景。由一系列操作(或事件)步骤组成。定义了角色与系统之间的交互,以实现某种目标。

如果不了解事务具体的含义。建议先阅读另一篇文章:浅谈事务理解

数据库事务是指(数据库)查询的逻辑单元。它通常由多个查询组成,是一个不可分割的整体。从应用角度而言,它还是一种能够简化编程模型的工具。因为即便任务在执行期间遇到异常,也无需担心数据完整性问题。而这完全得益于对 ACID 特性的支持。ACID 是数据库对事务作出的承诺。确保事务在并发异常崩溃等环境下仍然能够满足数据完整性

engins
图片截取自 MySQL 9.x.x
在 MySQL 中,事务机制(包括:Local、XA主要InnoDB提供支持。它是一个MVCC1存储引擎。其 MVCC 实现中,最为关键的概念是Undo Logs。因为事务的 并发回滚空间释放 等都以它为基础。 在事务环境下,MVCC 会为相关记录生成Undo Logs(链表),用于提高并发性能一致性(如:快照读)。此外,因为要支持快照读2,所以 delete 操作实际上并不会马上执行物理删除,而是会等待相关 Undo Logs 被完全弃置后才执行(。该任务称为:purge)。

通常情况下,人们在谈论关系型数据库事务时,本质是在关注对 ACID 特性的理解和应用。所以理解好 ACID 特性其实绝对有利于更好地去设计编程模型应用数据库

原子性(Atomicity)承诺将事务(中的所有查询)视为一个不可分割的整体。事务单元内的查询要么同时成功,要么同时失败。即便是遇到极端情况(如:电源故障、错误、崩溃)。这种承诺主要依赖于 Undo LogsRedo Log3。譬如 InnoDB 突然崩溃,但内存状态(Log Buffer)未能及时落盘。那么 MySQL 就会在重启时(接受请求前)进入恢复模式。它会尝试从 Redo Logs 中重做已经成功提交的事务,使其恢复到崩溃前的状态。其次还会利用 Undo Logs 回滚不完整的事务。

一致性(Consistency)在不同上下文中会有不同的解释。在当前 ACID 上下文中,可将该承诺理解为确保数据的约束规则 不会遭到破坏。这里指的约束规则其实就是不变性。譬如 主键唯一键外键枚举数据类型 等。事务在执行期间一旦违反不变性就会触发回滚操作,并且会向客户端抛出异常(例如当发生唯一建重复时,就会抛出 DuplicateKeyException)。该承诺同样依赖 Undo LogsRedo Log 来实现。另外需要重点注意,ACID一致性仅能确保(同库)数据定义层面的不变性,而无法确保用例逻辑层面的不变性。后者只能在应用代码中施加设计策略来解决。例如将操作限制在单个本地事务中,或引入分布式事务来确保原子性

理解例子:用户ab共用一个电子钱包(假设余额为100元),而且他们在相互不知情的情况下同时消费(假设a消费了70元,而b消费了50元)。在该场景下,其数据库事务会存在并发性。虽然感观上人们会认为不会发生消费额度大于余额的情况。但事实上,在编程的世界中所有的任务都会被编排成明确的步骤。就消费这一现实行为,其实就等同于(1)读取余额值,然后(2)从余额值中减去消费额度,最后(3)将剩余额度作为余额值。所以该案例最终会因为事务并发而出现不可预计的结果。譬如最终余额小于0储蓄机构损失20元。而且这两种可能性其实潜藏着不同的一致性概念:ACID一致性(确保数据不变性)逻辑一致性(确保逻辑不变性。对于当前上下文而言,就是不应该发生“超额消费”和“让储蓄机构造成损失”等情况)。最终余额小于0本质上是脏读问题4,事务本身就能够解决。可以给余额字段添加约束规则(譬如改用无符号数值类型)并使用READ COMMITTED以上的ACID隔离性即可。而储蓄机构损失20元则属于更新覆盖问题。因为是先读后写,所以一旦a事务先于b事务更新余额就会发生该问题。解决方案通常有两种:使用排他锁5对余额进行读写互斥 或 应用乐观锁6策略对记录进行版本控制。重点注意,即便使用串行化(Serializable)ACID隔离性也无法解决更新覆盖问题。因为串行化下读操作仍然存在并发性。所以它即不能避免先读后写,亦无法解决覆盖更新(。因为更新覆盖问题并没有违反数据不变性)。

隔离性(Isolation)可用于控制并发事务(计算结果)之间的可见性。承诺事务在并发期间可做到相互隔离7。InnoDB 为事务提供了4种隔离性。用隔离强度由低到高排列后如下:

  • READ UNCOMMITTED 隔离级别最低,但并发性能最好。因为不会使用快照读,所以能够读取到其他并发事务未提交的计算结果,存在脏读隐患。
  • READ COMMITTED 通过应用Undo Logs快照能够解决脏读问题。但它会引出另一种隐患,不可重复读8。因为该级别下的查询会始终读取最新版本的快照,而该版本的快照可能是其他并发事务所生成的(,所以相当于存在提交级别的脏读)。
  • REPEATABLE READ 是 InnoDB 默认的隔离级别。能够解决不可重复读问题,因为该级别下的查询会始终读取(相同SQL)第一次查询所生成的版本快照。注意,InnoDB 在该隔离级别下并不会出现幻读9 问题。因为默认会使用next-key lock来锁定记录(行)和范围间隙。
  • SERIALIZABLEREPEATABLE READ 大致相同。区别在于关闭 autocommit 后,前者会自动为查询谓语(where)添加 for share(即lock in share mode),从一致性非锁定读转为一致性锁定读。所以实际上事务之间并不会真正地串行执行。该级别的隔离性最好的,但性能较差。

除了 Undo Logs 之外,还可以使用锁来进行隔离。

级别
描述
record锁定记录行对应的索引。参考:共享锁(S)- select … for share 或 select … lock in share mode独占锁(X)- select … for update
gap允许范围查询时在索引之间进行锁定。可用于防止并发插入。注意,gapREAD COMMITTED 隔离级别下会有所限制(具体参考:Gap Locks)。
next-key实际上是recordgap的组合,用于范围查询场景。REPEATABLE READ 隔离级别下会自动启用
table独占锁(X),lock tables … write
intention解决recordtable的兼容问题。会应用级别锁,所以用户无需理会。
auto-inc控制AUTO_INCREMENT列值,受innodb_autoinc_lock_mode控制

持久性(Durability)是判断一个数据库系统是否可靠的重要指标。它承诺一旦事务被成功提交,即便发生故障(例如:断电、崩溃)也会将其状态持久化

innodb_architecture
图片源自:InnoDB Architecture
在 InnoDB 中,用于确保ACID持久性的机制主要是 Doublewrite BufferRedo Log。 当发生意外退出时,InnoDB 就会执行崩溃恢复流程。它会先尝试用 Doublewrite Buffer 来修复损坏的数据页(例如:崩溃前未能写入文件的数据页),以确保数据最终能被正确持久化。 能这样做因为 InnoDB 将 Buffer Pool10 数据落盘之前,会先将其写到 Doublewrite Buffer。而 Doublewrite Buffer 则会以大块顺序的方式写入日志文件中(,所以会非常高效)。 但若果 Doublewrite Buffer 无法用于修复的话,那么崩溃恢复就会改用 Redo Log 来重做事务。 但极端情况下,可能 Redo Log 也无法解决问题。此时数据就可能会永久丢失。但这种情况其实比较罕见,除非基础设施的质量非常差。 所以为了以防万一,最好是定期进行数据备份。

拓展:为什么只读场景也需要开启数据库事务?

确保读一致性。

参考


  1. 不同类型的关系型数据库对于 MVCC 的实现和支持会有所差异。 ↩︎

  2. 快照读也称为一致性非锁定读,它会从 Undo Logs 中读取记录的快照版本。另外,for updatefor share(或lock in share mode)updatedelete 这些称为一致性锁定读。 ↩︎

  3. Redo Log 是一种用于崩溃恢复的逻辑日志(属于预写日志记录,即WAL),用于重做未提交事务的状态。InnoDB 会透过后台线程将事务信息(Log Buffer)持续并顺序地写入到日志文件(如:#innodb_redo)。注意,日志文件并不会持续膨胀,其机制会使用 checkpoint 对旧内容进行截断(。这里的“旧内容”是指那些已经提交并落盘的事务数据)。另外,checkpoint 还被 InnoDB 用于刷新 Buffer Pool。 ↩︎

  4. 脏读:事务并发期间,读取到其他未提交事务的计算结果。 ↩︎

  5. 排他锁:泛指for update操作。 ↩︎

  6. 乐观锁:为记录添加一个版本标识字段,然后对编程模型进行约束。约束规则:1)更新记录时需要先读取版本标识。2)只有标识没有被改变过的情况下才更新记录(这一点通常依赖于ACID一致性)。 ↩︎

  7. ACID隔离性仅能控制并发期间的可见性,所以它无法解决与事务提交相关的问题(如更新覆盖)。此外,ACID隔离性是 MVCC 的主要应用场所,因为 MVCC 本身就是一种用来提高事务并发性能的机制。 ↩︎

  8. 不可重复读:重复执行相同的查询,但出现结果不一致的情况。 ↩︎

  9. 幻读:一种特殊的不可重复读,主要针对insertdelete操作。指对前后两次相同查询的结果进行比较,出现或消失了部份记录。 ↩︎

  10. Buffer Pool:用于缓存表和索引的数据(,以提高查询效率)。在专用服务器上,通常建议占 80% 的物理内存。配置参考:Buffer Pool Configuration ↩︎

Dik Tam
作者
Dik Tam
只是喜欢写代码。