vlambda博客
学习文章列表

MySQL 事务实现简析

什么是事务

事务是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。

事务的特性

事务的特性由 ACID 组成。

  • 原子性:(Atomicity)事务作为一个整体被执行,包括在其中的对数据库的操作要么全部被执行,要么都不被执行。
  • 一致性(Consistency)事务应确保数据库的状态从一个一致状态转变为另一个一致状态。抑制状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(Isolation)多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability)已被提交的事务对数据库的修改应该永久保存在数据库中

事务的实现方式

原子性实现

故障随时可能发生,比如掉电,机器重启,OOM 等。如何保证执行了一半的事务能够回滚,是原子性最需要关注的问题。

在计算机领域,任何问题都可以通过增加一个中间层来解决。

在数据库中,通常是将变更先写入到日志中,等到日志落盘成功,则认为该事务执行成功。

原子性的实现依赖于 undo log。当向数据库执行语句时,存储引擎会生成一条针对该操作的反向 log,这个 log 就被称为 undo log。比如插入一条数据的反向操作是删除该数据;修改一条数据的反向操作是增加一条该数据的原始数据的 SQL。

当执行回滚操作时,发现 undo log 中有尚未提交的事务,进行回滚即可。

持久性实现

持久性指当事务提交之后,对数据库的影响就是永久的,不会发生丢失的现象。

持久性最简单的方式就是每次将数据写入磁盘,当返回写入成功时,认为事务提交成功。但是每次与磁盘交互会大大降低系统的效率。

为了平衡访问效率,MySQL 引入了 Buffer Pool,它是基于内存的,访问速度快。但是如果掉电,内存中的数据将会丢失。这时就引入了 redo log。在对 Buffer Pool 进行修改的过程中,对应记录也会写到 redo log 中。此时就代表事务提交成功,即使现在发生掉电、重启,redo log 也已经写入到磁盘当中,不会发生丢失。

这里还需要提到一个 binlog。首先 redo log 是 InnoDB 存储引擎独有的,binlog 是 MySQL Server 独有的。因为历史原因,两者需要同时存在。需要先写入 redo log,再写入 binlog。这个过程被称为两阶段提交。

具体来说,数据写入 redo log 成功后,事务状态会变更为 prepare。然后将数据写入到 binlog。binlog 写入成功后,事务状态变为 commit。整体完成。

隔离性实现

隔离性是指事务内部的操作与其它事务是隔离的。类比于日常开发,其实就是临界资源的访问控制,在编程语言中,可以通过锁机制来保证并发安全。在数据库中,也有类似的实现,比如最高的隔离级别:串行化。就相当于对数据进行加锁,每个操作只能在获得锁之后才能被执行。

在这里,我们将隔离性面对的问题总结为:读读、读写、写写三种。

读读因为不涉及对数据的修改,所以不需要任何的并发保护。

写写需要强保护,这里通常是对数据加行锁,必须等到获取到锁之后才能被执行。如果不加锁,就会出现后面的操作覆盖前面的操作。比如两个事务同时对金额执行加一操作,如果不加锁,最后的结果很可能只增加了一。

读写比较复杂。读分为快照读和当前读两种。快照读可能会读到之前的数据,当前读强迫读当前最新的数据。比如 select * from t for update。

针对读写情况,提出了不同的隔离级别。读未提交、读已提交、可重复读。我们主要讨论读已提交和可重复读两种。

在主流数据库中,通常采用 MVCC(Multi Version Concurrency Control)的方式进行并发控制。

MVCC 在 MySQL 中主要由:隐藏列、undo log 和 Read View 实现。

  1. 隐藏列:在 MySQL 中,每条记录存在三个隐藏字段:本行数据的事务 id,指向上次修改的指针和隐藏的自增 ID。
  2. undo log:就是上文原子性中提到的 undo log。undo log 分为两类,一是 insert 的 undo log;二是 update 的 undo log(包含 update 和 delete)在 MVCC 中,只需要第二种的 undo log。隐藏列的上次修改的指针就是指向 undo log,指针相连形成了 undo log 的链表结构。
  3. Read View:在进行快照读的时候,会生成 Read View。Read View 相当于是当前时间节点的事务快照。 由最小事务 id、最大事务 id 的下一个 id 和当前活跃事务列表组成。 假设当前最新事务 id 为 DB_TRX_ID。① 如果 DB_TRX_ID < 最小事务 id。那么就意味着当前行对该事务是可见的。因为事务发生时,该行的最新记录早已经发生了。② 如果 最小事务 id < DB_TRX_ID < 最大事务 id + 1,继续判断 DB_TRX 是否在当前活跃事务列表中。如果不存在,则意味着当前行数据对该事务可见。因为 DB_TRX_ID 不活跃,也就是说它已经被提交了,所以可见。当上面的条件都不满足时,不可见。需要通过 DB_TRX_ID 找到回滚指针,再通过回滚指针找到新的 DB_TRX_ID,继续执行上面的流程,直到找到合适的版本满足要求。

读已提交和可重复读的区别就在于 Read View 的生成时机不同。读已提交是每次进行快照读的时候,系统就会生成一份新的 Read View;可重复读是只在事务开始后的第一次快照读生成 Read View。这就可以解释为什么读已提交能够读到不同的数据。这也是为什么 MySQL 的默认隔离级别是可重复读。

一致性实现

一致性其实不应该归于数据库的物理特性中,或者说,是由上面的原子性、隔离性和持久性共同努力实现了一致性。