一. InnoDB 事务隔离级别
参考网址: 《MySQL/InnoDB中的事务隔离级别》 《MySQL隔离级别》 《innodb当前读 与 快照读》 《MySQL的InnoDB的幻读问题》
1.1 数据异常现象
数据异常现象主要分三种:
- 脏读:事务 A 修改了数据但没有提交,事务 B 查询数据时可以查到事务 A 提交的数据,此时事务 A 回滚,此时事务 B 读取的数据与数据库中的数据不一致,即为脏读现象;
- 不可重复读:事务 A 查询数据,事务 B 修改 (update 或 delete) 了数据并提交,事务 A 再次用同样的语句查询,前后两次查询的数据不一致,即为不可重复读现象;
- 幻读:保证了同一个事务里查询的结果无论如何都是事务刚开始时的状态,保证了一致性,不会出现不可重复读的现象。但是,如果另一个事务同时提交了新数据,本事务再更新时,就会“惊奇的”发现了这些新数据,貌似之前读到的数据是“鬼影”一样的幻觉。
幻读比较难以理解,如下图所示,可以很好的解释幻读现象:
1.2 隔离级别
参考地址:《快照读、当前读和MVCC》
SQL 的隔离级别有四种:
- 读未提交 (Read uncommitted):会出现脏读、不可重复读、幻读;
- 读已提交 (Read committed):会出现不可重复读、幻读;
- Oracle 默认隔离级别;
- 一个事务内操作一条数据,可以查询到另一个已提交事务操作同一条数据的最新值;
- Oracle 默认隔离级别;
- 可重复读 (Repeatable Read):会出现幻读;
- Mysql 默认隔离级别;
- 每个事务只关注自己事务开始查询到的数据值,无论事务查询同一条数据多少次,该数据被改了多少次,都只查询到事务开始之前的数据值;
- 在 InnoDB 中用多版本控制 MVCC 的方式,保证了可重复读,而且可以防止幻读;但是 InnoDB 幻读时保证的数据一致性是快照读,也就是历史数据(见[第十六章](# 十六. 当前读与快照读));
- 串行 (Serializable):无;
- 注:串行化是悲观锁的理论实现,它对读加共享锁,对写加排他锁,读写分离。并发能力很差。
注:隔离级别与事务视图 readView 的关系,在[第十六章](# 十六. 当前读与快照读)中说明。
标准的隔离级别中,Oracle 只有 Read committed, Serializable 两种,此外还有 ReadOnly, WriteOnly 两种级别。其中 ReadOnly 是 Serializable 的子集。
1.3 事务的特性
参考地址:《从银行转账失败到分布式事务:总结与思考》
事务是一组 SQL 语句组成的,基本含义是一组 SQL 语句要么全都执行,要么全都不执行。有 ACID 四种特性:
- Atomicity 原子性:一组 SQL 语句要么全都执行,要么全都不执行;
- Consistency 一致性:数据的完整性约束不被破坏。可分为实体完整性、参照完整性、用户自定义完整性几种。
- 实体完整性:即主属性不为空;
- 参照完整性:外键必须存在于原表中;
- 用户自定义的完整性:比如定义某列值不能为空 (NOT NULL),列值唯一 (unique),是否满足 boolean 表达式(如岁数 age 一定在 [0, 150] 之间);
- 对于转账操作 A -> B,虽然 A, B 在这次转账的事务操作前后账户的总和一定,但这是应用层面的一致性,而不是数据库保证的一致性。这里应用层面的一致性实际上是由转账要求的原子性保证的。
- Isolation 隔离性:数据库允许多个并发的事务同时对数据库进行读写;两个并发的事务执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。
- 通常隔离性依赖于加锁,或者多版本控制保证。见[本篇第十六章](# 十六. 当前读和快照读);
- Durability 持久性:事务执行的结果,对数据库是永久性的,更改结果会被持久化到数据库,不会被回滚。
1.4 当前读和快照读
参考地址: 《innodb当前读 与 快照读》 《MySQL的InnoDB的幻读问题》 《快照读、当前读和MVCC》 《MYSQL(04)-间隙锁详解》
1.4.1 readView
快照读指的是读取一瞬间的数据,它在 InnoDB 中是通过多版本控制 MVCC 实现的,而 MVCC 是由事务视图 readView 实现的。Read View 查询同一条数据,因为 readView 是针对同一条数据生成的视图,每个 sql 语句查询某条数据时,都是查询最新 readView 的该条数据的值。
1.4.2 快照读
快照读的意思如字面意思一样,拿到的数据像是照片一样,反映了一瞬间的数据情况。快照读通常在普通的 select 方法中使用,且通常 select 方法不加 lock inshare mode
之类的锁。快照读是基于事务视图 readView 实现的,对于不同的事务隔离级别,readView 实现如下:
- 读已提交:事务中每个 SQL 语句生成一个 readView,这样事务内多个 SQL 语句会生成多个 readView,其中每条 SQL 执行时,都是查询最新 readView 的值;
- 所以会出现不可重复读的现象:事务 A 查询数据,事务 B 修改 (update 或 delete) 了数据并提交,事务 A 再次用同样的语句查询,前后两次查询的数据不一致;
- 可重复读:在事务开始的时候生成一个 readView,同一个事务内的多条查询 SQL 查询同一条数据时,读取到的 readView 都是同一个,查询某条数据的值也是同一个值;
- 比如事务 A 查询主键 id = 1 的行数据列 age = 10,不管事务 B 是否对该 age 值做出改变,事务 A 的多条查询 SQL 语句,查询 age 的值一定一直都是 age = 10;
在可重复读级别下,快照读是基于 MVCC 和 undo log 实现的,多个 readView 组成一个回滚日志 undo log。在该级别下,MVCC 完全解决了重复读,也在一定程度上避免了幻读,但是这种避免幻读的方式,是利用快照读的特性,在某事务开始时的第一个 select 生成一个 readView,该 readView 某种意义上算是第一个 select 时的历史数据。对事务 A 使用快照读的方式,表面上看避免了幻读,但如果其他事务 B 修改了数据,事务 A 再修改数据,然后事务 A 再查询数据,这时候事务 A 就会出现由事务 B 修改的数据,即事务 B 修改的数据并没有实时显示。 要完全避免这种现象,需要使用当前读的方式。
1.4.3 当前读
当前读可以读取最新的数据,完全避免了可重复读和幻读现象,它保证数据的一致性,同一个事务内部读取某一条数据时,数据都是一样的。 在可重复读级别下,当前读是通过行锁 (record lock) 与间隙锁 (gap lock) 实现的。以两个正在进行的事务 A, B 进行举例,其中事务 A 两条 SQL 语句,且第二条是 insert 语句,事务 B 是一个 insert 语句:
- 事务 A 开始时生成一个 readView (id = n),执行第一个 SQL 语句时,读取的是当前的 readView 值 (id = n);
- 事务 B 开始,首先生成 id = n 1 的 readView;
- 事务 B 使用索引进行插入(或 update 等操作)时,InnoDB 会在事务 B 中将当前行与上一个行加锁,对当前行用行锁 (record lock) 加锁,对上一行用间歇锁 (gap lock) 加锁(锁住一部分区域数据);
- 事务 A 执行第二个 SQL 即 insert 语句,这时候由于事务 B 还没有提交,所以没有释放数据锁,此时阻塞等待;
- 事务 B 执行完毕,释放锁,事务 A 的第二个 SQL 获取锁,读到当前最新的数据 (readView id = n 1);
这样就实现了读取最新的数据,即当前读。
典型的当前读操作:
- update
- delete
- insert
- select … lock in share mode
- select … for update