ACID特性的实现原理
ACID特性的实现原理
1、背景(什么是事务)
如上图所示,MySQL服务器逻辑架构从上往下可以分为三层:
第一层:处理客户端连接、授权认证等。
第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。
第三层:存储引擎,负责MySQL中数据的存储和提取。MySQL中服务器层不管理事务,事务是由存储引擎实现的。MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。
那什么是事务呢?
数据库的事务是指一组sql语句
访问各种数据项的数据库逻辑处理单元,在这组 SQL 操作中,要么全部执行成功,要么全部执行失败。
- 举个经典的例子就是转账,事务 A要进行转账,那么,转出的账号要扣钱,转入的账号要加钱,这两操作要么同时执行成功,要么都全部执行失败。为得就是确保数据的一致性。
事务想要做到什么效果?
可靠性
:数据库要保证当insert或update
操作时抛异常,或者数据库crash
的时候需要保障数据的操作前后的一致,想要做到这个,我需要知道我修改之前和修改之后的状态,所以就有了undo log
和redo log
。并发处理
:也就是说当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到啥程度得看业务系统的场景了,实现这个就得用MySQL的隔离级别。
三个重要知识点: 日志文件(redo log 和 undo log)
,锁技术
以及MVCC
2、四大特性(ACID)
-
1、原子性
:(Atomicity):故名思议原子是最小的元素单位,所以一个事务是一个不可拆分的最小工作单元。整个事务操作要么全部执行成功,要么全部失败回滚。- 实现主要基于
undo log
日志实现的。
- 实现主要基于
-
2、一致性
:(Consistency):指数据库从一个一致性的状态转换到另外一个一致性的状态。要保证事务前后的状态要一致 -
3、隔离性
:(Isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的。执行尽可能不受其他事务影响;InnoDB默认的隔离级别是可重复读(repeatable red,RR)
。- RR的实现主要基于锁机制、数据的隐藏列、undo log和类next-key lock机制。
-
4、持久性
:(Durability):一旦事务提交,则其所做的修改就会永久保存到磁盘上。是执行结果一直生效。 -
- 实现主要基于
redo log
日志。
- 实现主要基于
2.1、原子性(Atomicity)
2.1.1、定义
事务的原子性就如原子操作一般,表示事务不可再分,其中的操作要么都做,要么都不做。
- 如果事务中一个SQL语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。只有0和1,没有其它值。
- 事务的原子性表明事务就是一个整体,当事务无法成功执行的时候,需要将事务中已经执行过的语句全部回滚,使得数据库回归到最初未开始事务的状态。
- 事务的原子性就是通过
undo log日志
进行实现的。当事务需要进行回滚时,InnoDB引擎就会调用undo log日志进行SQL语句的撤销,实现数据的回滚。
2.1.2、undo log(回滚日志)
- 1、undo log:实现原子性的关键,是当事务回滚时能够
撤销所有已经成功执行的sql语句
。- 当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
- 2、undo log:属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo
log的内容做与之前相反的工作:- 对于每个insert,回滚时会执行delete;
- 对于每个delete,回滚时会执行insert;
- 对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
实例:
2.2、持久性(Durability )
2.2.1、定义
事务的持久性是指当事务提交之后,数据库的改变就应该是永久性的,而不是暂时的。这也就是说,当事务提交之后,任何其它操作甚至是系统的宕机故障都不会对原来事务的执行结果产生影响。
事务的持久性是通过InnoDB存储引擎中的redo log
日志来实现的
2.2.2、redo log(重做日志)
redo log存在的背景
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool)
,Buffer Pool
中包含了磁盘中部分数据页的映射
,作为访问数据库的缓冲:
- 当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为
刷脏
)。
Buffer Pool
的使用大大提高了读写数据的效率
,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
定义和作用
redo log
:是InnoDB引擎层的日志,用来记录事务操作引起数据的变化
,记录的是数据页的物理修改
。
打个比方。数据库中数据的修改就好比你写的论文,万一哪天论文丢了怎么呢?以防这种不幸的发生,我们可以在写论文的时候,每一次修改都拿个小本本记录一下,记录什么时间对某一页进行了怎么样的修改。这就是重做日志。
redo log被引入来解决MySQL宕机
- 1、当
数据修改
时,除了修改Buffer Pool
中的数据,还会在redo log
记录这次操作; - 2、当
事务提交
时,会调用fsync接口
对redo log进行刷盘
。如果MySQL宕机,重启时可以读取redo log
中的数据,对数据库进行恢复。redo log
采用的是WAL(Write-ahead logging,预写式日志)
,所有修改先写入日志,再更新到BufferPool
,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。InnoDB
引擎对数据的更新,是先将更新记录写入redo log
日志,然后会在系统空闲的时候或者是按照设定的更新策略再将日志中的内容更新到磁盘之中。这就是所谓的预写式技术(Write Ahead logging)
。这种技术可以大大减少IO操作的频率,提升数据刷新的效率。
脏数据刷盘
脏数据:指内存中未刷到磁盘的数据。
redo log日志的大小是固定的,为了能够持续不断的对更新记录进行写入,在redo log日志中设置了两个标志位置,checkpoint和write_pos,分别表示记录擦除的位置和记录写入的位置。redo log日志的数据写入示意图可见下图。
当write_pos
标志到了日志结尾
时,会从结尾跳至日志头部进行重新循环写入。所以redo log
的逻辑结构并不是线性
的,而是可看作一个圆周运动
。write_pos
与checkpoint
中间的空间可用于写入新数据,写入和擦除都是往后推移,循环往复的。
当write_pos追上checkpoint
时,表示redo log
日志已经写满。这时不能继续执行新的数据库更新语句,需要停下来先删除一些记录,执行checkpoint规则
腾出可写空间。
- checkpoint规则:checkpoint触发后,将buffer中脏数据页和脏日志页都刷到磁盘。
redo log中最重要的概念就是缓冲池buffer pool
,这是在内存中分配的一个区域,包含了磁盘中部分数据页的映射,作为访问数据库的缓冲。
- 当请求读取数据时,会先判断是否在缓冲池命中,如果未命中才会在磁盘上进行检索后放入缓冲池;
- 当请求写入数据时,会先写入缓冲池,缓冲池中修改的数据会定期刷新到磁盘中。这一过程也被称之为刷脏 。
脏日志刷盘
redo log日志在记录时,为了保证日志文件的持久化,也需要经历将日志记录从内存写入到磁盘的过程。
redo log日志可分为两个部分,一是存在易失性内存中的缓存日志redo log buff
,二是保存在磁盘上的redo log日志文件redo log file
。
为了确保每次记录都能够写入到磁盘中的日志中,每次将redo log buffer
中的日志写入redo log file
的过程中都会调用一次操作系统的fsync
操作。
fsync函数:包含在UNIX系统头文件#include <unistd.h>中,用于同步内存中所有已修改的文件数据到储存设备。
redo log日志的写入过程可见下图。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
1、刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
2、刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
实例:
start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400;
// 生成 重做日志 amount=400
update finance set amount = amount + 400;
commit;
2.3、隔离性(Isolation)
2.3.1、定义
隔离性:研究的是不同事务之间的相互影响
。即事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 严格的隔离性,对应了事务隔离级别中的
Serializable (可串行化)
,但实际应用中出于性能方面的考虑很少会使用可串行化。
隔离性追求的是并发读写
情形下事务之间互不干扰。简单起见,我们仅考虑最简单的读操作和写操作(暂时不考虑带锁读等特殊操作),那么隔离性的探讨,主要可以分为两个方面:
- 1、一个事务写操作对另一个事务写操作的影响:
锁机制保证隔离性
- 2、一个事务写操作对(另一个事务)读操作的影响:
MVCC保证隔离性
(面试大热点)
2.3.2、锁机制(后续详细介绍)
隔离性:要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。
锁机制的基本原理:
-
1、事务在修改数据之前,需要先获得相应的锁;
-
2、获得锁之后,事务便可以修改数据;
-
3、该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
锁的分类:
锁机制并不是一个陌生的概念,在许多场景中都会利用到不同实现的锁对数据进行保护和同步。而在MySQL中,根据不同的划分标准,还可将锁分为不同的种类。
按照粒度划分:行锁、表锁、页锁
按照使用方式划分:共享锁、排它锁
按照思想划分:悲观锁、乐观锁
粒度:指数据仓库的数据单位中保存数据的细化或综合程度的级别。细化程度越高,粒度级就越小;相反,细化程度越低,粒度级就越大。
MySQL按照锁的粒度划分可以分为行锁、表锁和页锁。
- 1、表锁在操作数据时会锁定整张表,并发性能较差;
- 2、行锁则只锁定需要操作的数据,并发性能好。
- 3、页锁:粒度介于行级锁和表级锁中间的一种锁,表示对页进行加锁。
- MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
- MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
注意
由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。
- MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。
如何查看锁信息
select * from information_schema.innodb_locks; #锁的概况
show engine innodb status; #InnoDB整体状态,其中包括锁的情况
2.3.3、并发读写问题
在并发情况下,MySQL的同时读写可能会导致三类问题,脏读、不可重复度和幻读。
(1)脏读
:当前事务中读到其他事务未提交
的数据,也就是脏数据。
以上图为例,事务A在读取文章的阅读量时,读取到了事务B为提交的数据。如果事务B最后没有顺利提交,导致事务回滚,那么实际上阅读量并没有修改成功,而事务A却是读到的修改后的值,显然不合情理。
(2)不可重复读
:在事务A中先后两次读取同一个数据
,但是两次读取的结果不一样
。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
以上图为例,事务A在先后读取文章阅读量的数据时,结果却不一样。说明事务A在执行的过程中,阅读量的值被其它事务给修改了。这样使得数据的查询结果不再可靠,同样也不合实际。
(3)幻读
:在事务A中按照某个条件先后两次查询数据库
,两次查询结果的行数不同
,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了
。
以上图为例,当对0<阅读量<100
的文章进行查询时,先查到了一个结果,后来查询到了两个结果。这表明同一个事务的查询结果数不一,行数不一致。这样的问题使得在根据某些条件对数据筛选的时候,前后筛选结果不具有可靠性。
2.3.4、事务隔离级别
SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:
2.3.5、MVCC(多版本并发控制,后续详细介绍)
MVCC全称Multi-Version Concurrency Control
,即多版本的并发控制协议。RR解决脏读、不可重复读、幻读等问题,使用的是MVCC。
- MVCC实际上就是通过
数据的隐藏列
和回滚日志(undo log)
,实现多个版本数据的共存。这样的好处是,使用MVCC进行读数据的时候,不用加锁,从而避免了同时读写的冲突。
InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间(或删除时间), 当然存储的并不是实际的时间值,而是系统版本号。
–摘自《高性能Mysql》这本书对MVCC的定义
下面的例子很好的体现了MVCC的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据。
另外,InnoDB实现的隔离级别RR时可以避免幻读现象的,这是通过next-key lock机制实现的。这里简单讲讲吧。
next-key lock实际上就是行锁的一种,只不过它不只是会锁住当前行记录的本身,还会锁定一个范围。比如上面幻读的例子,开始查询0<阅读量<100的文章时,只查到了一个结果。next-key lock会将查询出的这一行进行锁定,同时还会对0<阅读量<100这个范围进行加锁,这实际上是一种间隙锁。间隙锁能够防止其他事务在这个间隙修改或者插入记录。这样一来,就保证了在0<阅读量<100这个间隙中,只存在原来的一行数据,从而避免了幻读。
总结
概括来说,InnoDB实现的RR,通过锁机制、数据的隐藏列、undo log和类next-key lock,实现了一定程度的隔离性,可以满足大多数场景的需要。不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离。
2.4、一致性(Consistency)
2.4.1、定义
一致性:数据库总是从一个一致性的状态转移到另一个一致性的状态
。事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
- 数据库的完整性约束包括但不限于:
实体完整性
(如行的主键存在且唯一)、列完整性
(如字段的类型、大小、长度要符合要求)、外键约束
、用户自定义完整性
(如转账前后,两个账户余额的和应该不变)。
下面举个例子:zhangsan 从银行卡转400到理财账户
2.4.2. 实现
一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。
实现一致性的措施包括:
- 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
- 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
- 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致
参考
1、https://zhuanlan.zhihu.com/p/60723043
2、https://zhuanlan.zhihu.com/p/145430661
3、https://blog.csdn.net/lianhuanezha/article/details/107219807