Skip to content

MySQL ACID实现机制

本文介绍事务四大特性ACID的实现机制。

1. Durability-持久性

持久性的定义:一旦事务提交,其所做的修改将永久保存在数据库中,即使系统发生崩溃或电源故障也不会丢失。

实现持久性的原理:Redo log(重做日志) + Doublewrite Buffer(双写缓冲区)

我们以下面的例子来说明实现持久性的机制:

sql
begin;  -- 开启事务
update t set name='a' where id=1;  -- 修改数据,id是主键
commit;
  1. 首先手动开启事务,那么系统不会自动提交DML操作;
  2. 然后执行update语句:
    • 首先通过主键索引找到数据页,如果该页不在缓冲池中,则将该页加载进缓冲池中;
    • 然后在该页上执行修改操作,将name改为‘a’;
    • 将数据页标记为脏页;
  3. 记录修改操作,并保存到Log Buffer中
  4. 执行commit操作,此时MySQL会将Log Buffer中的内容刷新到磁盘中(由 innodb_flush_log_at_trx_commit 控制),刷新成功后才会给用户返回操作成功的提示。这样,操作记录就持久化到磁盘中了,并且由于日志是顺序写的,内容较少,所以速度是快的,不会影响体验。
  5. 哪怕此时系统崩溃,内存中的数据丢失,之后也可以通过Redo log恢复数据,保证了提交了的事务是不会丢失的。

Doublewrite Buffer(双写缓冲区)的作用是为了解决部分写失效问题:InnoDB 存储引擎的数据读写最小单位是页 (Page),通常是 16KB。操作系统读写磁盘的最小单位通常是 4KB 或 8KB。当 InnoDB 将一个 16KB 的数据页从内存写入磁盘时,如果操作系统层面的写操作(例如分 4 个 4KB 的块写入)在中间发生中断(比如服务器掉电、操作系统崩溃),就可能导致磁盘上的数据页只写入了一部分,形成一个不完整或损坏的页。一个被部分写入的页是无效的,而且更糟糕的是,仅仅依靠 Redo Log 无法恢复这种损坏的页。Redo Log 记录的是对数据页的逻辑物理修改(例如“将页 X 的偏移 Y 处的值从 Z 改为 W”),它假设被修改的页本身是完整的。如果页本身已经损坏,Redo Log 无法将其恢复到一个一致的、可用的状态。

具体解决方法可以查看前一篇文章:InnoDB存储引擎

2. Atomicity-原子性

原子性的定义: 一个事务中的所有操作,要么全部成功执行,要么全部不执行(回滚到事务开始前的状态)。没有中间状态。

实现原子性的原理:Undo Log(重做日志)

以下面的例子讲解Undo Log的使用

sql
begin;
update t set name='a' where id=1;
update t set age=20 where id=1;
update x set address = '重庆' where name='张三';
rollback;

假设现在开启一个事务,事务ID为999,那么执行三条update语句时,会产生三条Undo Log记录,与事务999相关联,示意图如下:

image-20250530194427118

当执行rollback语句时,会找到与事务999相关联的Undo Log记录,执行其中的内容,达到回滚的效果。

Undo log 记录分为两类:

  • Insert Undo Log:记录 INSERT 操作。
  • Update Undo Log:记录 UPDATEDELETE 操作。

3. Isolation-隔离性

隔离性的定义:多个并发事务之间互不干扰,一个事务的中间状态对其他事务不可见。事务看起来就像是串行执行一样。

实现隔离性的机制:锁(Locking)+ 多版本并发控制(MVCC)

3.1 MVCC

之前我们已经详细介绍过锁了,这里主要介绍MVCC。

MVCC,Multi-Version Concurrency Control,多版本并发控制,是指维护一个数据的多个版本,使得读写操作没有冲突。MVCC的实现依赖数据库记录中的三个隐藏字段、Undo Log和ReadView。

在介绍MVCC之前,先介绍三个隐藏字段。在MySQL中,为了维护的需要,添加了三个隐藏字段:DB_TRX_IDDB_ROLL_PTRDB_ROW_ID

image-20250530192512323

当多个事务对同一条记录执行写操作时,会形成一条Undo Log版本链,通过DB_ROLL_PTR相关联,链表的头部是最新的记录,链表的尾部是最早的记录:

image-20250530201109894

接下来介绍ReadView。先理解两个概念:

  • 当前读:读取的是记录的最新版本,并且读取时还要保证其他并发事务不能修改当前记录,所以会对读取的记录加锁。如select ... for shareselect ... for updateinsertupdatedelete都是一种当前读;
  • 快照读:简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

进行快照读时,就会生成一个ReadView对象,其中包含了四个关键字段:

image-20250530202132271

当事务对某条数据进行快照读时,根据生成的ReadView对象,和Undo Log链中记录版本数据中的trx_id进行比较,就能判断是否可以访问该版本数据:

image-20250530202846952

3.2 不同隔离级别

  • READ UNCOMMITTED-读未提交:既不会加锁,也不会使用MVCC。

  • READ COMMITTED-读已提交:使用加锁机制和MVCC机制。

    在事务中,每一次执行快照读时,会生成新的ReadView对象

    例如:

    在事务5中第一次执行快照读,生成一个ReadView对象,然后根据Undo Log版本链中查找可以读的记录,找到(红框):

    image-20250530204037402

    在事务5中再次执行快照读,又生成一个新的ReadView对象,再根据新的ReadView去Undo Log版本链中查找可以读的记录,找到另一条记录(红框):

    image-20250530204331397

  • REPEATABLE READ-可重复读:使用加锁机制和MVCC机制。

    仅在事务中第一次执行快照读时生成ReadView对象,后续服用该ReadView对象。

    例如:

    在事务5中执行两次快照读,第二次直接复用第一次的ReadView,所以和第一次的结果相同:

    image-20250530204703742

  • SERIALIZABLE-串行化:使用加锁机制。

4. Consistency-一致性

一致性的定义: 事务从一个数据库的合法状态(满足所有约束,如唯一性、外键、检查约束等)转换到另一个合法状态。

实现一致性的机制:很大程度上是原子性和隔离性的结果。

  • 原子性保证了数据不会出现“部分完成”的混乱状态。

  • 隔离性保证了并发操作不会导致数据冲突和不一致。

参考

[1] https://catkang.github.io/2021/10/30/mysql-undo.html

[2] https://www.bilibili.com/video/BV1Kr4y1i7ru

[3] https://www.bilibili.com/video/BV16VVRzuEvL