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 实现。
-
隐藏列:在 MySQL 中,每条记录存在三个隐藏字段:本行数据的事务 id,指向上次修改的指针和隐藏的自增 ID。 -
undo log:就是上文原子性中提到的 undo log。undo log 分为两类,一是 insert 的 undo log;二是 update 的 undo log(包含 update 和 delete)在 MVCC 中,只需要第二种的 undo log。隐藏列的上次修改的指针就是指向 undo log,指针相连形成了 undo log 的链表结构。 -
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 的默认隔离级别是可重复读。
一致性实现
一致性其实不应该归于数据库的物理特性中,或者说,是由上面的原子性、隔离性和持久性共同努力实现了一致性。