ACID的实现原理
引言
ACID是事务的特点也是必须的要求,只有保证ACID事务的执行才不会出错,分别是原子性、一致性、隔离性和持久性。我们知道典型的MySQL事务是这样执行的:
- start transaction 开启事务
- commit 提交事务
- rollback 回滚事务
注意两个默认机制:
- 如果没有显示开启事务,每条SQL都是单独的事务
- 自动提交机制
下面我们就来分析一下ACID是如何实现的?以及它和锁机制、隔离级别的关系。
实现原理
1、原子性(Atomicity)
原子性就是说事务是一个不可分割的基本单位,其中的操作要么全部执行,要么都不执行,其实就是rollback的实现机制,原子性实现的原理是通过undo log。
undo log是逻辑日志,它记录的是每条sql。当事务对数据库进行修改时,InnoDB 会生成对应的 undo log,如果事务执行失败调用了rollback,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。
对于每个 insert,回滚时会执行 delete。
对于每个 delete,回滚时会执行 insert。
对于每个 update,回滚时会执行一个相反的 update,把数据改回去。
2、持久性(Durability)
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。重点就是如何保证数据库宕机数据不受影响。实现原理是通过redo log
要想深入了解redolog,需要事先了解MySQL的存储引擎是怎么从磁盘读取数据,又是如何把数据刷回磁盘的?这里以InnoDB为例,由于磁盘IO速度很慢,因此InnoDB不直接与磁盘打交道,而是通过Buffer Pool缓冲池,以此来加速读和加速写。
加速读指的是读取时会优先从Buffer Pool读取,如果 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool。
加读写指的是当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中,这一过程称为刷脏。
但是如果buffer中保存的数据还没刷新到磁盘数据库就宕机了,会造成数据永久丢失。于是引入redo log解决这个问题,原理是WAL,先写log,再写buffer,并且每次事务提交都会把redo log刷新到磁盘。这个时候如果数据库宕机,也可以通过redo log的记录恢复所有数据。
你可能会有的两个疑问:
-
为什么buffer pool中的数据要定期刷脏,如果每次事务提交都刷新到磁盘,就不需要redo log
因为每次修改的数据随机,buffer pool刷脏过程是随机IO,速度很慢,而redo log是追加操作,数据都是连续的,属于顺序IO。第二个原因是刷脏都是以数据页为单位的,一个小修改都要整页写入,而redo log中只包含真正修改的部分,无效IO减少。
-
redo log 和 bin log的关系,bin log是否与持久性有关?
完全无关,两者的层次和维度都不相同。bin log是server端的,用于备份和恢复数据、主从复制等,而redo log是innodb特有的,用于保证异常情况下数据安全。当然redo log的二阶段提交也是必要的,用来保证redo log和bin log的一致性。
3、隔离性(Isolation)
1. 引言
这是个重头戏,涉及到很多方面的内容
隔离性指的是事物内部的操作与其他事务是隔离的,并发执行的事务之间不能互相干扰。不同的隔离级别事务并发程度也不相同,能解决的问题也不同。一般来说,隔离级别越高并发程度越低,因为要加不同的锁。
MySQL隔离级别 -- 可能产生的问题:
- 读未提交 -- 脏读、不可重复读、幻读
- 读已提交 -- 不可重复读、幻读
- 可重复读 -- 幻读
- 串行化 -- 无
先对各个级别加锁情况做个介绍,让你有个基本概念:
- 读未提交级别:不需要加任何锁,因此它的并发程度最高,但同时也会引发各种并发问题
- 读已提交级别:读不需要加锁,但是写需要加排它锁/MVCC
- 可重复读级别:有两种不同的实现方式,一是悲观锁即读加共享锁,写加排它锁,这种方式并发程度低;二是乐观锁即MVCC,它的优势是不加锁,使用undo log和视图的概念实现,并发程度高
- 串行化:读加共享锁,写加排他锁,读写互斥
可以看到,随着隔离级别的提高,假的锁也更多,并发程度自然更低。实际应用时要根据业务需求,选择最合适的隔离级别。
其实隔离性本质上就是解决两个问题:
-
(一个事务)写操作对(另一个事务)写操作的影响:只能通过锁机制(当前读,读取最新数据)
-
(一个事务)写操作对(另一个事务)读操作的影响:目标就是不通过加锁也能解决,目前最优解是MVCC(快照读,不需要最新的数据)
2. 锁的分类
接下来简单介绍一下数据库的锁,可以从两个维度进行分析:
一、从锁范围分,可以把锁分为:全局锁、表级锁、行级锁,锁的精度逐渐增加,锁精度越高,需要同时锁住的数据越少,并发程度越高
二、从锁的作用分,可以把锁分为:共享锁(其他锁只能读不能写)、排他锁(其他锁不能读也不能写),很明显,共享锁的并发程度更高
3. MVCC的实现原理
主要依靠数据的隐藏列(也可以称之为标记位)和 undo log。其中数据的隐藏列包括了该行数据的版本号、删除时间、指向 undo log 的指针等等。当读取数据时,MySQL 可以通过隐藏列判断是否需要回滚并找到回滚需要的 undo log,从而实现 MVCC。
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下:
-
SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
- 1、InnoDB 只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行
- 2、行的删除操作的版本一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前,行没有被删除
符合了以上两点则返回查询结果。
-
INSERT时,保存当前事务版本号为行的创建版本号
- InnoDB 为每个新增行记录当前系统版本号作为创建 ID。
-
DELETE时,保存当前事务版本号为行的删除版本号
- InnoDB 为每个删除行的记录当前系统版本号作为行的删除 ID。
-
UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行
这里简单做下总结:
-
insert 操作时 “创建时间”=DB_ROW_ID,这时,“删除时间 ”是未定义的;
-
update 时,复制新增行的“创建时间”=DB_ROW_ID,删除时间未定义,旧数据行“创建时间”不变,删除时间=该事务的 DB_ROW_ID;
-
delete 操作,相应数据行的“创建时间”不变,删除时间=该事务的 DB_ROW_ID;
-
select 操作对两者都不修改,只读相应的数据
4. 可重复读的实现
快照读(MVCC)
当你执行 begin 开启事务之后,MySQL 会拍下像下图这样的快照:
- 当读取的记录的事务版本号小于当前事务版本号,并且不再活跃事务中,说明修改该记录的事务已经被提交,此记录可读
- 当读取的记录的事务版本号大号当前事务版本号,说明修改该记录的事务已经未提交,此记录不可读,通过undo log往前找,直到找到第一个 trx_id 等于或者小于自己事务 ID 的记录为止
当前读(间隙锁)
与其他数据库,MySQL数据库的可重复读可以解决幻读问题,原理就通过间隙锁,为某行记录添加行锁时同时为附近的记录也添加行锁,虽然这种实现方式很多时候会锁住不需要锁的区间。如下所示:
5. 读已提交的实现
得益于MVCC,读已提交的隔离级别也可以通过undo log+视图的机制实现,避免频繁加锁
具体的实现方式是每执行一个SQL都要重新创建视图,根据视图各变量和记录事务ID判断此记录可不可读
为什么要每条SQL都要重复创建视图呢?因此读已提交隔离级别下可以读到其他事务已提交的事务,所以每条SQL执行前都要更新视图中的活跃事务ID。
4、一致性(Consistency)
致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)
可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。
总结
本文从事务的四大特性出发,结合日志机制、锁机制以及隔离级别,简单梳理了事务四大特性ACID的实现原理以及它们之间的关系,其中最重要的是隔离性的实现,保护经典乐观锁MVCC以及视图机制,希望能对你理解MySQL事务有一点帮助。
作者实力有限,若有错误之处,欢迎留言指出。最后祝大家中秋快乐!