mysql是如何解决不可重复读和幻读的
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 |
查询的时候需要满足以下两个条件:
- 当前事务的版本号需要 大于或等于创建版本号create_version
- 当前事务的版本号需要小于删除的版本号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: 只针对唯一索引,可以理解为是行锁+间隙锁, 锁住一个区间和记录本身, 整体区间范围 左开右闭
当事务进行当前读的时候, 使用间隙锁或临键锁,锁住一个区间,这样当其他事务在这个区间内进行删除或新增操作的时候,就只能阻塞,需要等待当前事务释放这个锁。 从而保证前后两次读取到的数据一致。