Innodb作为现版本Mysql中默认的存储引擎,其事务相关知识是非常重要的,也是面试高频点,因此这篇将介绍下事务的ACID属性,具体实现方式及对ACID的理解
面试题
先抛出一点常见的面试题,如果这些面试题有不太熟悉的,可以看下下文中相关部分,带着问题去阅读
- 数据库的事务是什么,有哪些属性
- Mysql隔离级别
- ACID中为什么要强调一致性
- Mysql怎么解决不可重复读的问题
- Mysql在可重复读下会有幻读情况吗
- 可重复读的底层实现
- 怎么解决幻读的问题
事务介绍
事务:事务简单来说是一组SQL语句,要么同时执行,要么同时不执行,相当于买东西时候捆绑销售,要么全部买,要么全部不买。
而事务有四大金刚(特性),就是鼎鼎有名的ACID,下面分别介绍下ACID
ACID含义
原子性(Atomicity)
原子性指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
因为之前原子是不可分割的单位,因此一般原子性或原子操作含义就是不可再拆分
j = i++或Person p = new Person()这种虽然看着是一条语句,但在编译时及执行时,是会被拆分的,
一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态,保证数据的可靠性
关于一致性的理解后面会详细介绍
隔离性(Isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
就像自己在上厕所时,把门锁了,让其他人进不来,这样自己就不会被其他人打扰,虽然在Java中有自旋锁这种“不要脸”的会不时来看看门开了没
持久性(Durability)
持久性指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响
就像礼物给了出去,就是别人的,不能再要回来了
并发事务问题
对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种并发问题:
更新丢失:多个事务选择同一行,最后的更新会覆盖之前的更新。在提交事务前,不能访问同一文件,可避免。
脏读:对于两个事务T1、T2,T1读取了已经被T2更新但还没有提交的字段,之后若T2回滚,T1读取的内容就是临时且无效的
不可重复读:对于两个事务T1、T2,T1读取了一个字段,然后T2更新了该字段,之后T1再次读取同一个字段,值就不同了
幻读:对于两个事务T1、T2,T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行,之后如果T1再次读取同一个表,就会多出几行
脏读与幻读相对比,脏读侧重于字段的更新,幻读侧重于行的插入。
解决并发事务问题
数据库提供了不同的隔离级别来解决上面提到的脏读、不可重复度与幻读的问题
数据库的隔离级别:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题
数据库提供的4种事务隔离级别
| 隔离级别 | 描述 |
|---|---|
| READ UNCOMMITTED(读未提交数据) | 允许事务读取未被其他事务提交的变更,脏读、不可重复度和幻读的问题都会出现 |
| READ COMMITTED(读已提交数据) | 只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题仍可能出现 |
| REPEATABLE READ(可重复读) | 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但幻读的问题仍然存在 |
| SERIALIZABLE(串行化) | 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入,更新和删除操作,所有并发问题都可以避免,但性能十分低下 |
Oracle支持的2种事务级别:READ COMMITTED,SERIALIZABLE。Oracle默认的事务隔离级别为READ COMMITTED
Mysql支持4种事务级别,Mysql默认的事务隔离级别为REPETABLE READ
事务演示
查看当前的隔离级别
1 | select @@tx_isolation; |
更改隔离级别
1 | set session transaction isolation level 待修改的级别【read uncommitted|read committed|repeatable read|serializable】; |
事务的开启与结束
步骤1:开启事务
1 | SET autocommit=0; |
步骤2:编写事务中的sql语句(select insert update delete)
语句1;
语句2;
…
【设置回滚点:
savepoint 回滚点名;】
步骤3:结束事务
1 | commit;提交事务 |
savepoint 节点名;#设置保存点
数据库表设置
演示表为account,有id,name,salary三个值,建表语句如下所示
1 | CREATE TABLE `account` ( |
选择数据库,建立表,更改编码
1 | set names gbk; |
插入一条数据
1 | insert into account values(1,'张三',100); |
演示脏读
设置隔离级别为读未提交
1 | set session transaction isolation level read uncommitted; |
| 事务A | 事务B | |
|---|---|---|
| T1 | SET autocommit=0; | SET autocommit=0; |
| T2 | select * from account where id = 1; | |
| T3 | update account set username=’赵六’ where id = 1; | |
| T4 | select * from account where id = 1; | |
| T5 | rollback; | |
| T6 | select * from account where id = 1; |
这样,事务A在T2时刻看的数据username为张三,事务B在T3更改了数据后,事务A在T4查看,此时id=1的数据username为赵六,但当事务B在T5回滚事务后,事务A在T6看到是原来的数据,username为张三,这样便出现了脏读
演示不可重复读
设置隔离级别为读已提交
1 | set session transaction isolation level read committed; |
| 事务A | 事务B | |
|---|---|---|
| T1 | SET autocommit=0; | SET autocommit=0; |
| T2 | select * from account where id = 1; | |
| T3 | update account set username=’赵六’ where id = 1; | |
| T4 | select * from account where id = 1; | |
| T5 | commit; | |
| T6 | select * from account where id = 1; |
这样,事务A在T2时刻看的数据username为张三,事务B在T3更改了数据后,事务A在T4查看,此时id=1的数据username仍为张三,解决了脏读问题,但当事务B在T5提交事务后,事务A在T6看到是修改后的数据username为赵六,这样便出现了不可重复读
演示幻读
设置隔离级别为可重复读
1 | set session transaction isolation level repeatable read; |
| 事务A | 事务B | |
|---|---|---|
| T1 | SET autocommit=0; | SET autocommit=0; |
| T2 | select * from account; | |
| T3 | insert into account values(2,’王五’,50); | |
| T4 | commit | |
| T5 | update account set balance=10; |
当事务A在T2读取数据,只有1条,当事务B在T3插入一行数据后,事务A在T5更新的时候,会影响2条数据,出现了幻读。但是如果此时事务A去读取数据,仍然只有1条。原因是select读是快照读,MVCC解决了快照读的幻读问题。而update为当前读,在当前读下仍然会出现幻读情况。
同时,使用select * from account for update;这种当前读的select也会出现幻读。
ACID实现
介绍完ACID后,下面介绍ACID是如何被实现的
原子性(A)的实现
回忆一下:原子性为一个事务是不可分割的工作单位,操作要么一起做,要么都不做,若一个sql执行失败,已经执行的语句需要回滚。
而原子性的实现依靠的是undo log(回滚日志),实现原子性的关键为可以撤销成功执行的sql语句。当事务要对数据库进行修改时,InnoDB生成对应的undo log,若事务执行失败或调用了roll back,可以使用undo log来将数据修改为回滚前的状态。
undo log是逻辑日志,记录sql执行相关信息,属于存储引擎层。当发生回滚时,InnoDB根据undo log的内容做与之前相反的操作:对insert,进行delete;对delete,进行insert;对update,执行相反的update,将数据改回去。
持久性(D)的实现
回忆一下:持久性为事务一旦提交,对数据库的改变应该是永久性的,接下来的其他操作或故障不应该对其有影响。
持久性的实现依靠的是redo log(重做日志),与undo log回滚日志一样属于InnoDB的事务日志。
InnoDB提供缓存Buffer Pool,包含磁盘中部分数据页的映射,作为数据库的缓冲,减少IO次数。数据库读取数据的逻辑是:先在Buffer Pool中读取,若缓冲中没有,从磁盘读取后放入缓冲。当向数据库写数据,先写入缓冲,Buffer Pool中修改的数据定期刷新到磁盘中(刷脏)。Buffer Pool的优点是提高了读写效率,但问题是,若MySQL宕机,会导致Buffer Pool中数据没有刷新到磁盘,数据丢失,持久性也就无法保证。因此redo log被引入。
数据修改时,除了在Buffer Pool中修改,也会在redo log中记录;redo log默认在事务提交时刷盘,也有每秒刷盘等。MySQL宕机后,重启时可以读取redo log中的数据,对数据库进行恢复。而所有修改先写入redo log,再更新到Buffer Pool,这样可以保证持久性。
而redo log可以保证持久性的关键是它比刷脏要更快,不然刷脏很快也没有必要使用redo log,redo log更快的原因是
- 刷脏是随机IO,修改数据随机;redo log是追加操作,为顺序IO(不用多个地方移动磁头)
- 刷脏以数据页为单位,MySQL默认页是16kb,对页上做小修改要整页写入;redo log只包含真正需要写入的数据(修改量更少)
隔离性(I)的实现
回忆一下:隔离性为事务的内部操作与其他事务隔离,并发执行的事务之间不能相互干扰。最严格的隔离性,对应事务隔离级别中的串行化。
主要考虑读与写之间的隔离性
- 事务A写操作对事务B写操作的影响:锁机制保证
- 事务写操作对事务B读操作的影响:MVCC保证
锁使用的是行锁+间隙锁,之后再详细介绍,先可简单理解为给要操作的那个数据上了锁,其他事务无法对其有影响
并发事务会产生脏读,不可重复读和幻读问题,对应的,数据库依靠前面所说的四个隔离级别来消除这些影响,但隔离级别越高,虽然读取问题变少,但性能也变差,一般数据库默认可重复读(MySQL)或读已提高(Oracle)。
而隔离级别解决脏读、不可重复读或幻读,依靠的是MVCC,即Multi-Version Concurrency Control,多版本并发事务控制。MVCC的特点是不同事务读取到的数据可能是不同的(多版本的),因为读为快照读(读取不是最新的)。最大的优先是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要依靠的是数据隐藏列(标记位),undo log和Read View(读视图)。
要实现多版本控制,需要保存有不同版本的数据,需要当前版本可以找到上一个版本的数据,和判断当前版本可以看到哪些版本的数据,分别依靠undo log,隐藏列与Read View。
隐藏列中包括
- 最近修改事务ID
- 回滚指针:配合undo log,指向此记录上一个版本
- 隐含的自增ID
- 实际还有删除flag隐藏字段,记录被更新或删除不是真的删除,而是flag变化
读取数据时,MySQL可以判断是否需要回滚并找到所需要的undo log,从而实现。
对MVCC有帮助的是undo log中记录了update 和 delete的日志,在快照读时有用。
当有新事务对数据进行了修改,新事务会有指针指向旧事务。
这样undo log形成一个单向链表,表头为最新的旧记录,表尾是最早的旧记录。
而Read View是在事务进行快照读的时候产生的读视图,维护了活跃事务的ID(自增,越新事务ID越大),用来判断当前版本的视图可以看到哪个版本的数据。其用来做数据的可见性判断,其可见性算法是,取出要修改数据的最新记录中的事务ID,与Read View中维护的活跃事务ID比较,若不符合可见性,则通过回滚指针找到undo log中上一条记录,直到找到符合可见性规则的(从表头到表尾)。
其判断的逻辑为:在Read View中维护了一系列生成时刻事务ID,其中有最小事务ID与已创建最大事务ID。
- 如果数据中当前事务ID小于生成时最小事务ID,说明此记录是在Read View前面就出现了,最新的数据是可以被看到的;若大于等于进入下一个判断
- 如果当前事务ID大于等于已创建最大事务ID,说明此记录是在Read View后才出现的,肯定对当前事务不可见;若小于,进入下一个判断
- 判断数据中事务ID是否仍在活跃事务之中,如果在,代表Read View时刻,事务还在活跃,没有commit,修改的数据当前事务也看不到;如果不在,说明事务在Read View前已经commit了,修改的结果对当前事务是可见的
如果一个数据被删除了,将版本链上的数据复制一份,更新修改事务ID,然后将标志位更改为true,如果查询到了已经删除的数据(flag为true),则不返回数据。
因此可以看出快照读的结果非常依赖于Read View生成的时机。
在读已提交下,每个快照读都会生成并获取最新的Read View,可重复读下同一个事务的第一个快照才生成Read View,之后快照读取的仍为同一个Read View。
个人小总结为:若是快照读生成的时机越早,更新的次数越少,越有可能对其他事务修改的数据不可见。
而对于可重复读下的幻读问题,如果是标准的SQL规范下,是会存在幻读的,但在Mysql的Innodb引擎下,依靠行锁+间隙锁与MVCC可解决快照读的幻读问题。
一致性(C)的实现
一致性为数据库完整性约束没有被破坏,事务执行的前后数据状态都是合法的。一致性是事务的最终目标,是依靠于原子性、隔离性和持久性来实现的。在数据库层面与应用层层面都需要来保障一致性。
有关一致性的理解
一致性既然是靠A,D,C来实现的(乌兹乌兹乌兹),为何还要将其单独放出来呢?因为A,D,C属于数据库级别的实现,是在数据库的功能下实现的,而一致性属于应用层,如转账的场景,A现有10元,给B转了50元,A的余额变为-40,这个转账过程是符合ADC过程的,但是不符合业务规定,即一致性是认为规定的应用层的约定,需要依靠程序+业务中某些规定来实现,因此一致性属性是非常重要也需要单独放出来的。
最后,再看下这些面试题,是否有了更清晰的认识呢~
- 数据库的事务是什么,有哪些属性
- Mysql隔离级别
- ACID中为什么要强调一致性
- Mysql怎么解决不可重复读的问题
- Mysql在可重复读下会有幻读情况吗
- 可重复读的底层实现
- 怎么解决幻读的问题
参考资料
深入学习MySQL事务:ACID特性的实现原理:https://www.cnblogs.com/kismetv/p/10331633.html
正确的理解MySQL的MVCC及实现原理:https://blog.csdn.net/SnailMann/article/details/94724197?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
如何理解数据库事务中的一致性的概念?:https://www.zhihu.com/question/31346392