本文主要记录了 MySQL ACID 的具体实现原理,包括原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)。

1. 概述

本文主要讲述数据库事务和 ACID 具体是什么,以及 MySQL 是如何实现它们的。

结论

ACID 4 个特性中:

  • 一致性(consistency)是目的;
  • 原子性(atomicity)、隔离性(isolation)、持久性(durability)是手段

即通过原子性、隔离性、持久性来保证一致性。

因此数据库必须实现 AID 三大特性才有可能实现一致性(C)

MySQL ACID 特性实现原理:

  • 原子性:主要基于 undo log
  • 持久性:主要基于 redo log
  • 隔离性:InnoDB 默认的隔离级别是 RR,RR 的实现主要基于锁机制(包含 next-key lock)、MVCC(包括数据的隐藏列、基于 undo log 的版本链、ReadView)
  • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障

数据库事务和 ACID 特性是个什么关系呢?

首先引用 维基百科 的解释:

  • 数据库事务:在数据库管理系统中,事务是单个逻辑或工作单元,有时由多个操作组成。在数据库中以一致模式完成的任何逻辑计算都称为事务

  • ACID:ACID是数据库事务的一组属性,旨在在出现错误、电源故障和其他事故的情况下保证数据的有效性。在数据库的上下文中,满足 ACID 属性的一系列数据库操作(可以将其视为对数据的单个逻辑操作)称为事务

事务就是在数据库中以一致模式完成的任何逻辑计算,而满足 ACID 属性的一系列数据库操作也可以称作事务。

即:满足 ACID 特性的操作就是事务。

2. ACID 具体实现

2.1 原子性(atomicity)

事务通常由多个语句组成,原子性保证每个事务都被视为一个单独的单元,要么完全成功,要么完全失败。即一个事务(transaction)中的所有操作,要么全部执行成功,要么全部不执行。

简单来说:原子性的结果就是没有中间状态,如果有中间状态则一致性就不会得到满足。

仔细想一想没有中间结果是不可能实现的,所以 MySQL 采用了曲线救国的方式:即执行失败后,可以回滚,保证不会出现部分成功的情况,通过 undolog 实现该特性。

事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

undo log 属于逻辑日志,它记录的是 sql 执行相关的信息。当发生回滚时,InnoDB 会根据 undo log 的内容做与之前相反的工作:对于每个 insert,回滚时会执行 delete;对于每个 delete,回滚时会执行insert;对于每个 update,回滚时会执行一个相反的 update,把数据改回去。

虽然从结果上看是保证原子性了,但是和真正的原子性还是有差距的,应为这样实现其实是有中间结果,在并发场景下其他事务可能会看到中间结果。

为了保证中间结果对外不可见,需要通过隔离性来保证并发场景下的正确性

  • 通过隔离性保证其他事务不会看到本事务的执行结果;
  • 当然 read uncommitted 级别则没有这个功能,所以一般不用该隔离级别。

MySQL 原子性实现原理

  • 通过 undolog 在失败时回滚保证在结果上是原子性的, 即没有中间状态。

  • 通过隔离性保证了在其他并发事务看来是原子性的,即中间状态对外不可见。

2.2 隔离性(isolation)

隔离性,指一个事务内部的操作及使用的数据对正在进行的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

正是它保证了原子操作的过程中,中间状态对其它事务不可见

隔离性有原子性的部分,但是更多的还是关注并发事务。

Mysql 隔离级别有以下四种(级别由低到高):

  • ReadUncommitted 读未提交
  • ReadCommitted 读已提交
  • RepeatableRead 可重复度
  • Serializable 串行化

隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

  • (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
  • (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

锁机制

按照粒度,锁可以分为表锁行锁以及其他位于二者之间的锁

  • 表锁在操作数据时会锁定整张表,并发性能较差;
  • 行锁则只锁定需要操作的数据,并发性能好。

但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。

MVCC机制

MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:

  • 1)隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
  • 2)基于undo log的版本链:前面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
  • 3)ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定。
    • 所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

TODO 隔离性是最复杂的,内容比较多,等后续展开细说。

2.3 持久性(durability)

持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

InnoDB 通过 redo log 重做日志保证了事务的持久性。

事务开始之后就产生 redo log,redo log 的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo log 文件中。

参数innodb_flush_log_at_trx_commit可设置在事务 commit 的时候必须要写入 redo log 文件。

redo log 具体原理:

  • 当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;
  • 当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。
  • redo log 采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到 Buffer Pool 。
  • 如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。

2.4 一致性(consistency)

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。

数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障

3. 小结

下面总结一下ACID特性及其实现原理:

  • 原子性:语句要么全执行,要么全不执行,即没有中间状态,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于 undo log;
  • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于 redo log;
  • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB 默认的隔离级别是 RR,RR 的实现主要基于锁机制(包含 next-key lock)、MVCC(包括数据的隐藏列、基于 undo log 的版本链、ReadView);
  • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障。

4. 参考

《MySQL技术内幕 InnoDB存储引擎》

《高性能MySQL》

https://blog.csdn.net/weixin_44015043/article/details/105217603

https://www.cnblogs.com/feixiablog/articles/8301798.html

https://www.cnblogs.com/jianzh5/p/11643151.html

https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html#auto_id_11

https://www.cnblogs.com/xinysu/p/6555082.html