快照读和当前读
快照读
快照读是指读取数据时不是读取最新版本的数据,而是基于历史版本读取的一个快照信息(mysql读取undo log历史版本) ,
快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁
当前读
当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的
(update、delete、insert、select ....lock in share mode、select for update)
MVCC如何解决幻读?
快照读下,通过Read view的方式解决了幻读,本文主要介绍快照读下的的幻读解决方式
当前读下,需要使用间隙锁来解决幻读,但本质上当前读就是为了查最新数据
如 select * from xxx where id > 1 for update
Read view是什么?
InnoDB的快照读并非创建数据的完整备份,而是创建了一份事务id相关的数据快照,结合undo_log来实现整体数据的快照读
read view主要储存了下列4项数据的快照
trx_ids: 当前系统活跃(未提交)事务版本号集合。
low_limit_id: 创建当前read view 时当前系统最大事务版本号+1。
为什么不是当前事务版本号+1:事务开始时刻为当前事务版本号+1,但读已提交级别的后续查询会创建新的readview,所以实际储存的是最大事务版本号+1
up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
creator_trx_id: 创建当前read view的事务版本号;
InnoDB如何实现可重复读和读已提交?
1)数据库已存记录
trx_id | 数据 | undolog |
0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
2)trx_id=2 修改数据id=1的数据 name=cjd11
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
读已提交场景
3)trx_id=3 开启查询,创建readview
trx_ids | low_limit_id | up_limit_id | creator_trx_id |
2,3 | 2 | 4 | 3 |
此时数据库数据为
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
trx_id = 2
>= low_limit_id(处于活跃的最小版本号) 且
< up_limit_id(当前系统最大事务版本号+1)且
!=creator_trx_id(当前事务版本号) 但
in trx_ids(正在活跃的事务版本号内)
故此数据不可见,根据undolog指针找到上一条记录trx_id=0 < low_limit_id,故数据可见
故该查询最终得到的数据为
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
4)trx_id=2 新增记录id =3; trx_id=4新增记录id=4
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
2 | id = 3; name = cjd3 |
|
|
|
|
|
4 | id = 4; name = cjd4 |
|
|
|
|
|
5)trx_id=3 开启查询,创建readview
trx_ids | low_limit_id | up_limit_id | creator_trx_id |
2,3,4 | 2 | 5 | 3 |
新增的id=3与id=4,因为事务版本号处于活跃,故都对该次查询不可见
故该查询最终得到的数据为
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
2 | id = 3; name = cjd3 |
|
|
|
|
|
4 | id = 4; name = cjd4 |
|
|
|
|
|
6)trx_id=4 修改id=4数据为name=cjd41,并提交事务,trx_id=3创建数据name=cjd5
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
2 | id = 3; name = cjd3 |
|
|
|
|
|
4 | id = 4; name = cjd41 | 0xxxxxxx | → | 4 | id = 4; name = cjd4 |
|
3 | id = 5; name = cjd5 |
|
|
|
|
|
7)trx_id=3 开启查询,创建readview
trx_ids | low_limit_id | up_limit_id | creator_trx_id |
2,3 | 2 | 5 | 3 |
事务4提交后,数据name=cjd41 对事务3可见,name=cjd5事务号为创建者自身也可见,故最终查询得到的数据为
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
2 | id = 3; name = cjd3 |
|
|
|
|
|
4 | id = 4; name = cjd41 | 0xxxxxxx | → | 4 | id = 4; name = cjd4 |
|
3 | id = 5; name = cjd5 |
|
|
|
|
|
可重复读场景
3)可重复读场景下,无论第几次查询均使用事务开启时创建的read view,但可以读到后续自身事务创建的数据
trx_ids | low_limit_id | up_limit_id | creator_trx_id |
2,3 | 2 | 4 | 3 |
故可重复读得到的查询结果为
trx_id | 数据 | undolog | | trx_id | 数据 | undolog |
2 | id = 1; name = cjd11 | 0xxxxxxx | → | 0 | id = 1; name = cjd1 |
|
0 | id = 2; name = cjd2 |
|
|
|
|
|
2 | id = 3; name = cjd3 |
|
|
|
|
|
4 | id = 4; name = cjd41 | 0xxxxxxx | → | 4 | id = 4; name = cjd4 |
|
3 | id = 5; name = cjd5 |
|
|
|
|
|
读已提交每次查询创建新的read view,可重复读使用的第一次创建的read view
这也是为什么可重复读能解决幻读,而读已提交不能解决幻读的原因