Appearance
通过例子理解MySQL的隔离级别 
我们先快速回顾一遍mysql当中的事务隔离级别。
事务隔离级别概述 
MySQL的事务隔离级别有四种,分别是:
- 读未提交(READ UNCOMMITTED)
 - 读已提交(READ COMMITTED)
 - 可重复读(REPEATABLE READ)
 - 串行化(SERIALIZABLE)
 
要想了解怎么做的,我们就要知道没有他们可能导致的问题,有:
- 脏读
 - 不可重复读
 - 幻读
 
并发问题详解与实例 
脏读 
假设我们有一个accounts表:
| id | user_name | balance | 
|---|---|---|
| 1 | zhangsan | 1000 | 
在读未提交级别下:
事务A开始,执行:
sql
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;这个时候balance变成800了,但是还没有提交。
这个时候事务B开始,继续更新位置,他会认为这里A已经进行修改了,也就是800-200 = 600:
sql
USE test_dirty_read;
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
COMMIT;这个时候balance变成了600,然后事务A发生了回滚,也就是说在事务B中读取到的是不正确的值800,他没有确定要被更改,这就是脏读。
不可重复读 
我们在读已提交的隔离级别下做测试。
先新建表:
| id | name | age | 
|---|---|---|
| 1 | 张三 | 25 | 
在第一个会话当中:
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT age FROM users WHERE id = 1;在第二个会话当中修改age为26并提交:
sql
START TRANSACTION;
UPDATE users SET age = 26 WHERE id = 1;
COMMIT;再次在会话A中进行查询:
sql
SELECT age FROM users WHERE id = 1;结果是26,我们无法读取到事务开始时的25了,这就是不可重复读,寓意就是在同一个事务中多次读取同一行数据,结果可能会不一样。
幻读 
不可重复读是对于单条记录而言的,而幻读是对于多条记录而言,对于某一个范围内的数据而言的。
我们先插入数据:
| id | name | price | 
|---|---|---|
| 1 | 电脑 | 6000.00 | 
| 2 | 手机 | 4000.00 | 
在第一个会话当中:
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT COUNT(*) FROM products WHERE price < 5000;结果为1,因为只有手机。
在会话2中:
sql
START TRANSACTION;
INSERT INTO products (name, price) VALUES ('鼠标', 100.00);
COMMIT;回到会话1中再查询这个范围的数据,会发现count的值为2。
这样我们就把所有的隔离级别可能出现的问题都介绍了一遍,也就是说明白了是什么。
隔离级别对比 
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 | 主要实现机制(InnoDB) | 
|---|---|---|---|---|---|
| 读未提交 | 允许 | 允许 | 允许 | 最高 | 无锁,直接读最新数据 | 
| 读已提交 | 避免 | 允许 | 允许 | 较高 | 每次读生成新 ReadView,写操作加行级排他锁 | 
| 可重复读 | 避免 | 避免 | 避免 | 中等 | 事务开始时生成 ReadView,结合间隙锁/临键锁防止幻读 | 
| 串行化 | 避免 | 避免 | 避免 | 最低 | 强制串行执行,读加共享锁,写加排他锁 | 
注意,这些所谓的问题,都是针对于某一个事务而言的:
- 脏读的意思是,对于这个事务,他读取到的数值可能是别人修改过的。
 - 不可重复读的意思是,对于这个事务,他在同一个事务中多次读取同一行数据,结果可能会不一样。
 - 幻读的意思是,对于这个事务,他在同一个事务中多次读取某个范围内的数据,结果可能会不一样。
 
对于数据库来说,没有这些问题意味着,从事务开始的时候数据是什么样子的,无论外部做任何修改,当前事务读取到的值都是这个开始时的值,无论是范围还是单行。
问题产生的根本原因 
这里可能会有困惑,这么看脏读和不可重复读没什么区别啊。
- 脏读是在对方事务未提交的情况下的。也就是说无论对方提交没提交,当前事务读取到的值都是不确定的。
 - 不可重复读是对方事务提交了之后,当前事务读取到的值可能会不一样。
 
接下来我们总结为什么会有这些问题:
- 脏读(READ UNCOMMITTED):SELECT 毫不关心锁和提交状态,直接读最新数据。
 - 不可重复读(READ COMMITTED):SELECT 只读已提交的数据,但每次 SELECT 都会获取最新的已提交数据版本。
 - 幻读(READ COMMITTED):SELECT 只读已提交数据,但由于查询的是一个范围,当其他事务在这个范围内插入或删除数据并提交后,下次查询时结果集的行数会发生变化。
 
InnoDB的实现机制 
MVCC与ReadView机制 
为了解决脏读问题,提供了读已提交,他在每一次select的时候生成一个快照,也就是ReadView,这个ReadView包含了当前所有未提交的事务表,如果某行的事务为未提交状态,那么就不去读取这一行。这样别的未提交修改就不会影响到当前事务的读取。
为了解决不可重复读问题,提供了可重复读的隔离级别。在事务一开始就会生成一个ReadView,而不是在每次select的时候生成,这样就可以保证在这个事务中读取到的值都是一致的。也就是在这个事务开始时的快照。
间隙锁与临键锁机制 
有一个问题就是幻读,innodb是怎么解决的呢?
InnoDB使用间隙锁和临键锁解决这个问题。
假设我们有以下数据:
| id | name | 
|---|---|
| 1 | 电脑 | 
| 5 | 手机 | 
| 10 | 耳机 | 
| 15 | 键盘 | 
| 20 | 鼠标 | 
| 25 | 显示器 | 
然后我们的查询是:
sql
SELECT ... WHERE id > 6 AND id < 12会找到第一条大于6的记录10,继续往后直到找到第一条大于等于12的记录15,锁住查询到的10,锁住小于等于6的第一个索引,也就是5。锁住大于等于12的第一个索引也就是15,也就是锁住了(5,10]和[10,15)这两个范围。
临键锁并不会根据具体的范围来锁,而是锁住了查询到的记录和它前后相邻的索引值。
特殊情况:当前读vs快照读 
那么InnoDB的可重复读真的解决了幻读吗?
MySQL官方对于幻读的定义是,在一个事务中进行多次查询,会查询到不一样的数据就算是幻读。
假设我们的数据是:
| id | name | status | 
|---|---|---|
| 1 | joe | 1 | 
| 2 | jill | 1 | 
会话1执行select会查询到这两个 ↓ 会话2更新插入(3 bob 1)并提交 ↓ 会话1再次执行select还是会查询到一开始查询的两个。
但是如果这个时候会话1又执行了:
sql
UPDATE users SET name='huashen'最后就会显示:
| id | name | status | 
|---|---|---|
| 1 | huashen | 1 | 
| 2 | huashen | 1 | 
| 3 | huashen | 1 | 
第三条数据被会话2插入了,在会话1中被查到了。
这是违背MySQL官方认定的不幻读的定义的。
为什么会出现这种情况? 
这是因为在 UPDATE 的时候,它不是一个快照读,而是一个当前读(Current Read)。UPDATE 操作不会使用旧的 ReadView,而是会去读取最新的、已提交的数据,也就是说,它会看到被会话 2 插入的数据。UPDATE 会对这些数据加锁,并进行修改,因此 id=3 这条数据被它修改了。
在这个 UPDATE 操作之后,你再执行 SELECT 时,InnoDB 仍然使用最初的 ReadView。但是,根据 MVCC 的规则,一个事务可以读取自己修改过的数据。由于 UPDATE 已经将 id=3 的记录修改成了当前事务的版本,所以 SELECT 就可以看到这条数据了。
ReadView的本质 
ReadView本质上是一个快照范围,他规定了只能读到某一个范围内的数据,然后通过这个规定,可以知道哪些数据是可以读取的,哪些是不能被读取的。可以理解为存了多个版本的数据,通过版本号来区分哪些数据是可以读取的,哪些是不能被读取的。
上述问题在于因为update操作,所以这个修改后的数据也加入到了当前的事务快照中,所以你就可以读到这个事务2修改的数据了。
操作流程总结: update操作是当前读 → 读到最新数据 → 更新数据并加入到当前事务当中 → 可以在下次select中读取。
