数据库事务(InnoDB ACID)
用语 | 解释 |
---|---|
数据库 | 存取数据的系统。在当前文章中,泛指关系型数据库。 |
查询 | 泛指针对数据记录而执行的操作(,而非特指select)。 |
逻辑单元 | 在功能或语义上紧密相关、共同完成某个特定任务的一组代指令。 |
数据完整性 | 在数据的整个生命周期内,维护和保证其准确性和一致性。 |
数据库对象 | 指 database、table、view、index、procedure、function、trigger 这些。 |
表空间 | 保存数据库对象底层实际数据或结构的存储位置。 |
MVCC | 全称是多版本并发控制。一种用于提高事务并发访问性能的机制。 |
ACID | atomicity, consistency, isolation, durability 的简称。 |
不变性 | 泛指在某种生命周期内,固定不变(恒成立)的规则。 |
并发 | 进程之间拥有共享状态,但互相无法感知。 |
用例 | 系统接收外部请求(例如:用户输入)并做出响应的潜在场景。由一系列操作(或事件)步骤组成。定义了角色与系统之间的交互,以实现某种目标。 |
如果不了解事务具体的含义。建议先阅读另一篇文章:浅谈事务理解
数据库事务是指(数据库)查询的逻辑单元。它通常由多个查询组成,是一个不可分割的整体。从应用角度而言,它还是一种能够简化编程模型的工具。因为即便任务在执行期间遇到异常,也无需担心数据完整性问题。而这完全得益于对 ACID 特性的支持。ACID 是数据库对事务作出的承诺。确保事务在并发、异常、崩溃等环境下仍然能够满足数据完整性。delete
操作实际上并不会马上执行物理删除,而是会等待相关 Undo Logs 被完全弃置后才执行(。该任务称为:purge)。
通常情况下,人们在谈论关系型数据库事务时,本质是在关注对 ACID 特性的理解和应用。所以理解好 ACID 特性其实绝对有利于更好地去设计编程模型和应用数据库。
原子性(Atomicity)承诺将事务(中的所有查询)视为一个不可分割的整体。事务单元内的查询要么同时成功,要么同时失败。即便是遇到极端情况(如:电源故障、错误、崩溃)。这种承诺主要依赖于 Undo Logs 和 Redo Log3。譬如 InnoDB 突然崩溃,但内存状态(Log Buffer)未能及时落盘。那么 MySQL 就会在重启时(接受请求前)进入恢复模式。它会尝试从 Redo Logs 中重做已经成功提交的事务,使其恢复到崩溃前的状态。其次还会利用 Undo Logs 回滚不完整的事务。
一致性(Consistency)在不同上下文中会有不同的解释。在当前 ACID 上下文中,可将该承诺理解为确保数据的约束规则 不会遭到破坏。这里指的约束规则其实就是不变性。譬如 主键、唯一键、外键、枚举、数据类型 等。事务在执行期间一旦违反不变性就会触发回滚操作,并且会向客户端抛出异常(例如当发生唯一建重复时,就会抛出 DuplicateKeyException)。该承诺同样依赖 Undo Logs 和 Redo Log 来实现。另外需要重点注意,ACID一致性仅能确保(同库)数据定义层面的不变性,而无法确保用例逻辑层面的不变性。后者只能在应用代码中施加设计策略来解决。例如将操作限制在单个本地事务中,或引入分布式事务来确保原子性。
理解例子:用户a
、b
共用一个电子钱包(假设余额为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来锁定记录(行)和范围间隙。SERIALIZABLE
与REPEATABLE 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 | 行 | 允许范围查询时在索引之间进行锁定。可用于防止并发插入。注意,gap 在 READ COMMITTED 隔离级别下会有所限制(具体参考:Gap Locks)。 | |
next-key | 行 | 实际上是record 和gap 的组合,用于范围查询场景。在 REPEATABLE READ 隔离级别下会自动启用。 | |
table | 表 | 独占锁(X),lock tables … write | |
intention | 表 | 解决record 和table 的兼容问题。会应用级别锁,所以用户无需理会。 | |
auto-inc | 表 | 控制AUTO_INCREMENT 列值,受innodb_autoinc_lock_mode控制 |
持久性(Durability)是判断一个数据库系统是否可靠的重要指标。它承诺一旦事务被成功提交,即便发生故障(例如:断电、崩溃)也会将其状态持久化。
拓展:为什么只读场景也需要开启数据库事务?
确保读一致性。
参考
- Distributed transaction
- ACID
- MySQL Glossary
- The InnoDB Storage Engine
- InnoDB Storage Engine: What Is New in MySQL 9.0
- MySQL Server Logs
- START TRANSACTION, COMMIT, and ROLLBACK Statements
不同类型的关系型数据库对于 MVCC 的实现和支持会有所差异。 ↩︎
快照读也称为一致性非锁定读,它会从 Undo Logs 中读取记录的快照版本。另外,
for update
、for share(或lock in share mode)
、update
、delete
这些称为一致性锁定读。 ↩︎Redo Log 是一种用于崩溃恢复的逻辑日志(属于预写日志记录,即WAL),用于重做未提交事务的状态。InnoDB 会透过后台线程将事务信息(Log Buffer)持续并顺序地写入到日志文件(如:#innodb_redo)。注意,日志文件并不会持续膨胀,其机制会使用 checkpoint 对旧内容进行截断(。这里的“旧内容”是指那些已经提交并落盘的事务数据)。另外,checkpoint 还被 InnoDB 用于刷新 Buffer Pool。 ↩︎
脏读:事务并发期间,读取到其他未提交事务的计算结果。 ↩︎
排他锁:泛指
for update
操作。 ↩︎乐观锁:为记录添加一个版本标识字段,然后对编程模型进行约束。约束规则:1)更新记录时需要先读取版本标识。2)只有标识没有被改变过的情况下才更新记录(这一点通常依赖于ACID一致性)。 ↩︎
ACID隔离性仅能控制并发期间的可见性,所以它无法解决与事务提交相关的问题(如更新覆盖)。此外,ACID隔离性是 MVCC 的主要应用场所,因为 MVCC 本身就是一种用来提高事务并发性能的机制。 ↩︎
不可重复读:重复执行相同的查询,但出现结果不一致的情况。 ↩︎
幻读:一种特殊的不可重复读,主要针对
insert
和delete
操作。指对前后两次相同查询的结果进行比较,出现或消失了部份记录。 ↩︎Buffer Pool:用于缓存表和索引的数据(,以提高查询效率)。在专用服务器上,通常建议占 80% 的物理内存。配置参考:Buffer Pool Configuration ↩︎