4.事务
都是基于 InnoDB 进行解释。
- redolog + undolog 日志,是 InnoDB 的底层能力。
- binlog 是mysql server 层的能力。
1.数据库事务管理
我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合。由于数据操作在顺序执行的过程中, 任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作数据的并不可靠,需要在这种情况下进行回退。
事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。
2.ACID
2.1.什么是ACIO
事务的四大特性ACID:
- 原子性(atomicity): 事务的最小工作单元,要么全成功,要么全失败。
- 一致性(consistency): 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。
- 隔离性(isolation): 一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE(串行化)。
- 持久性(durability): 事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失。
2.2.如何保证ACID
- 原子性:是由undolog日志保证的,它记录了需要回滚的日志信息,事务回滚时撤销已经执行的SQL。使用锁确保同一时刻只有一个事务可以操作数据。
- 一致性:有其他三个特性共同保证的,程序代码要保证业务上的一致性。
- 隔离性:由MVCC来保证的
- 持久性:由redolog来保证,MySQL修改数据的时候会在redolog中记录一份日志,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失。
3.隔离级别
查看数据库默认隔离级别: select @@tx_isolation
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。 低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
- 读未提交:READ-UNCOMMITED,又称为脏读,一个事务可以读取到另一个事务未提交的数据。这种隔离级别最不安全。
- 读已提交:READ-COMMITED,又称为不可重复读,一个事务因为读取到另一个已提交的修改数据,导致当前事务中不同时间读取的数据不一致。
- 可重复读:REPEATABLE-READ,又称为幻读,一个事务可以读取到其他事务提交的数据,但是在RR隔离级别下,当前读取此数据只可读取一次, 在当前事务中,不论读取多少次,数据仍然是第一次读取的值,不会因为在第一次读取之后,其他事务对数据修改并提交了而发生不一致。
- 串行化:SERIALIZABLE,所有的数据库的读或写操作都是串行化的,当前隔离级别下只同一时刻只有一个事务在执行,所有事务都要排队等待
| 脏读 | 不可重复读 | 幻读 | |
|---|---|---|---|
| read uncommited(未提交读,1) | Y | Y | Y |
| read commited(已提交读,2) | N | Y | Y |
| repeatable read(可重复读,4) | N | N | Y(高版本是N) |
| serializable(串行化,8) | N | N | N |
- Oracle 默认 read commited
- Mysql 默认 repeatable read
很多时候,不可重复读并不是问题,并且RR级别存在间隙锁降低性能,所以在很多互联网场景使用RC
3.1.脏读(Drity Read)
某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
3.2.不可重复读(Non-repeatable read)
在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
3.3.幻读(Phantom Read)
- 前提条件:InnoDB引擎,可重复读隔离级别,使用当前读时。
- 表现:一个事务(同一个read view)在前后两次查询同一范围的时候,后一次查询看到了前一次查询没有看到的行。两点需要说明:
- 在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,幻读只在当前读下才会出现。
- 幻读专指新插入的行,读到原本存在行的更新结果不算。因为当前读的作用就是能读到所有已经提交记录的最新值。
- 幻读的影响
会造成一个事务中先产生的锁,无法锁住后加入的满足条件的行。产生数据一致性问题,在一个事务中,先对符合条件的目标行做变更, 而在事务提交前有新的符合目标条件的行加入。这样通过binlog恢复的数据是会将所有符合条件的目标行都进行变更的。
- 幻读产生的原因
行锁只能锁住行,即使把所有的行记录都上锁,也阻止不了新插入的记录。
- 如何解决幻读
将两行记录间的空隙加上锁,阻止新记录的插入;这个锁称为间隙锁。 间隙锁与间隙锁之间没有冲突关系。跟间隙锁存在冲突关系的,是往这个间隙中插入一个记录这个操作。
3.4.常见问题
事务隔离级别怎么实现的?
总结:锁+MVCC机制
- 读未提交:READ-UNCOMMITED,没有锁,也不隔离,最不安全。
- 读已提交:READ-COMMITED,有行锁,数据被锁时,别的事务无法更改,每次读取最新提交的数据。
- 可重复读:REPEATABLE-READ,有行锁,数据被锁时,别的事务无法更改,记录第一次读取时提交的数据(undo日志),之后在事务内都读取这个副本。 mysql进行了优化,引入next key lock ,解决了幻读的问题。
- 串行化:SERIALIZABLE,表锁,读写都串行,不需要MVCC。
https://www.cnblogs.com/Genesisx/p/9139175.html
脏读、不可重复读、幻读都是什么
- 脏读:读取到其他事务未提交的数据,数据可能不会持久化(回滚、丢弃、变化等)
- 不可重复读:针对一条数据,2次查询的数据发生了变化,重点应用在update和delete。只需要锁住满足条件的记录,针对的是单行数据
- 幻读:针对一条查询SQL,2次查询返回的数据量不同,重点应用在insert。 要锁住满足条件及其相近的记录,针对的是包括单行数据在内的一个区间的数据
解决方案:
- 可重复读,是为了解决在隔离级别是RC时,同一条数据两次读取的结果不同。解决的方案是在隔离级别RR,通过多版本控制(MVCC)实现的,不涉及到锁。
- 幻读,是在RR隔离级别时,某个范围内查询数据,两次查询出来的数据量不同。 解决方式是要锁住满足条件及其相近的记录,针对的是包括单行数据在内的一个区间的数据。 其实就是锁的机制,范围不一样的。也就是next-key锁。使用的时候,必须使用当前读;快照读的时候,只会加间隙锁。
可重复读怎么解决的?
在RC级别下,普通的查询SQL,每次读取的都是最新的数据,所以会出现不可重复度的问题。 在RR级别采用MVCC机制解决不可重复读。
MVCC原理,多版本并发控制,读取的时候有快照读和直接读,和锁没有关系。
Begin -- 开启事务
Select * from table_name where id=1; // 版本号是1
// 业务代码... // 别的事务将这条数据改了,版本号已经是2了
Select * from table_name where id=1; //
再查询,这条数据的版本号还是1,这样防止幻读,同时实现可重复度
Commit; -- 提交事务
幻读怎么解决的
MVCC只能保存第一次查询出来的数据是快照数据,如果查询条件的范围内,增加了数据,后面就出现幻读了。
第一个方式是事务隔离级别改成串行
第二个是:RR级别使用:next-key lock。
next-key locks由record locks(索引加锁/行锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)的结合, next-key lock 会锁定范围和自身行,比如select…where id<6,锁定的是小于6的行和等于6的行
mysql默认隔离级别是啥?为啥用这个?
默认是RR。因为这个级别下有mvcc可以解决不可重复读的问题。next-key lock解决幻读。
在RC级别下,不可重复读问题需要解决么? 不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle的默认隔离级别就是RC,你们改过Oracle的默认隔离级别么?
在RC级别下,主从复制用什么binlog格式? OK,在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!
互联网项目会选择哪个?为啥?
选择RC。原因:
- RR下存在间隙锁,这加大了死锁的概率。
- RR下如果查询未命中索引会锁表。而RC不会,RC只会锁行,所以并发效率更高
- 对于不可重复读的要求不高。毕竟oracle默认就是RC
4.MVCC
通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
PS: MVCC 就是快照读,不会加锁。
4.1.实现原理
在一个事务中,查询数据的时候,会创建一个read view。
核心知识:
- Read View 中四个隐藏字段。
- m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,"活跃事务"指的就是,启动了但还没提交的事务。
- min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
- max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
- creator_trx_id :指的是创建该 Read View 的事务的事务 id。
- 聚簇索引记录中两个跟事务有关的隐藏列
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:

- 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
- 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
- 如果记录的 trx_id 值在 Read View 的min_trx_id和max_trx_id之间,需要判断 trx_id 是否在 m_ids 列表中:
- 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
- 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
总结: MVCC只在REPEATABLE READ(可重复读) 和 READ COMMITTED(已读提交)这俩种隔离级别下适用,实现原理:
- 数据结构上的隐藏字段(最近一次DML的事务ID,回滚指针,删除数据的事务ID)
- undolog。记录当前事务操作前版本的数据,包括回滚指针,方便回滚。
- readView算法。查询时产生的一致性视图,由这份数据相关的所有事务ID组成的数组。通过对比事务ID大小,找到undo log中的数据。
4.2.常见问题
使用快照读的时候,会加锁吗?
快照读:
- SQL 通过主键或唯一索引查询到的单行数据,不会加锁。
- SQL 通过范围查询或者普通索引,可能查询多条数据,会加间隙锁。
当前读: 想要获得当前读的话,只有下面5种场景:
- select * from table where ? lock in share mode; 共享锁,别的事务可以读,但是不能写
- select * from table where ? for update; 独占锁、悲观锁
- insert;
- update;
- delete;
MVCC 解决了幻读吗?
MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的, 即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候, 会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
这两个解决方案是很大程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是无法解决的。
比如这个场景: 
不同隔离级别中的Read View作用
- 读未提交:没有Read View
- 读已提交:启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
- 可重复读:每次读取数据时,都会生成一个新的 Read View。
- 串行化:没有Read View,用不到
5.spring中的事务
隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。
- 本文作者:zhangxue
- 本文链接:https://lucksnow1989.github.io/article/7.数据库/【关系型】MySQL/4.事务.html
- 版权声明:本文为本人原创文章,遵循 CC 4.0 BY-NC-SA版权协议,转载请附上原文出处链接和本声明。