Mysql的默认隔离级别是可重复读。并且在该级别下, 也不会出现幻读(一般意义上的可重复读是存在幻读的), 那么Mysql是如何实现的呢?

不可重复读的解决方案

不可重复读定义:在同一个事务中, 前后两次相同的查询, 返回的结果不一样, 针对的是 update 的操作

结论:通过使用MVCC实现不可重复读。

每个事务在开始的时候, 都会创建一个递增的当前事务版本号。

mysql中每条记录都有两个隐藏字段:创建版本号create_version和删除版本号delete_version。

  • 当记录被insert的时候, 创建版本号就是当前事务的版本号, 删除版本号为null
  • 当记录被delete的时候, 将删除版本号设置为当前事务的版本号
  • 当记录被update的时候, 先进行delete,然后再进行add

对于update的操作, 举例说明一下, 假设刚开始的表数据如下:

id name create_version delete_version
1 小明 1 NULL
2 小红 3 NULL

现在要执行如下sql, 假设执行此sql的事务版本号为10:

update person set name='李梅' where id =1

执行完毕之后表中数据如下:

id name create_version delete_version
1 小明 1 10
2 小红 3 NULL
1 李梅 10 NULL

查询的时候需要满足以下两个条件:

  1. 当前事务的版本号需要 大于或等于创建版本号create_version
  2. 当前事务的版本号需要小于删除的版本号delete_version,或者当前删除版本号delete_version为NULL

这样, 就能阻止在同一个事务中读取到其他事务提交的数据, 从而实现了可重复读。

根据上述结论, 如果执行 select * from person where id=1的查询, 假设开启此查询的事务版本号是11, 此时得到的结果如下:

id name create_version delete_version
1 李梅 10 NULL

幻读的解决方案

幻读定义: 在同一个事务中,前后两次相同的查询, 返回的结果不一样, 针对的是 insert 和 delete 的操作。

结论:

  • 对于快照读, MVCC就能实现
  • 对于当前读, 是利用锁来实现的

首先说一下两种读的区别:

  • 快照读: 事务开始的时候,生成一个快照, 之后的数据都是从这个快照中读取。 普通的select语句就是快照读。
  • 当前读:读取数据的最新版本。insert、update、delete、select..for update 都是当前读。

由于快照读取的永远是事务刚开始的快照数据, 所以即使有其他事务进行了删除或新增操作,该事务也读取不到,所以天然的解决了幻读。

对于当前读, 首先要明白一个概念, 锁。

mysql 什么时候会加锁?

在进行insert、update、delete、select…for update操作。

需要注意的是,此处的锁指的是行锁, 都是针对索引进行加锁的, 如果没有索引,那么加的就是表锁, 锁住的是整个表。

mysql的行锁分为如下三种:

  • 记录锁 record lock: 只针对唯一索引或主键索引, 锁住的是单条记录
  • 间隙锁 gap lock: 只针对非唯一索引,锁住的是一个区间, 但并不包含该记录本身
  • 临键锁 next key lock: 只针对唯一索引,可以理解为是行锁+间隙锁, 锁住一个区间和记录本身, 整体区间范围 左开右闭

当事务进行当前读的时候, 使用间隙锁或临键锁,锁住一个区间,这样当其他事务在这个区间内进行删除或新增操作的时候,就只能阻塞,需要等待当前事务释放这个锁。 从而保证前后两次读取到的数据一致。