MVCC
Multiversion Concurrency Control,意味多版本并发控制,也就是MySql InnoDB引擎解决幻读问题的方案
MySql InnoDB默认隔离级别是可重复读,本文后续会围绕MVCC展开,简述MVCC解决该隔离级别问题的原理&方式
相关知识点链接
数据库隔离级别 -> 内容如题
MySql 共享锁 & 排他锁 -> 讲述SQL层面会用到的2种锁
MySql 行级锁 & 表级锁 -> 讲述不同存储引擎的锁类型
快照读
快照读又称为一致性读,指读取的是快照数据,MySql InnoDB 中不加锁的 SELECT 都属于快照读,即非阻塞读
SELECT * FROM table_name WHERE
上文知识点中会提到,发展出快照读是为了提高并发性能,快照读的实现是基于MVCC,避免了加锁操作从而降低了开销;在多版本控制中,快照读并不一定是最新版本的数据,可能是历史版本;快照读的前提是隔离级别可重复读,串行化级别下会退化成当前读
当前读
当前读获取的是最新数据,读取时要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,加锁语句可在上述链接 共享锁 & 排他锁 中查看
当前读带来的问题
事务A
select * from table where column > 1 for update
事务B
insert into table(column) values (2);
insert into table(column) values (3);
事务A在事务B之前使用当前读获取数据,事务B在事务A之后进行数据写入,在主库中执行此命令可能不会有问题,但是如果事务B的数据在事务A结束之前写入完毕,从库通过binlog同步,则在从库中事务A发生的改动可能对事务B的数据生效,导致主库从库数据不一致;
间隙锁
对于上述问题,MySql 引入了间隙锁来解决,其原理为,在事务A加锁是时,MySql会对其搜索的数据的间隙加上锁,即对所有column > 1的数据加锁,即使数据不存在,或者说,不允许任何事务对
column > 1进行写入,此时就能阻止事务B的执行,从而保证了主库从库数据一致
间隙锁带来的问题
在上述案例中,如果事务A迟迟不执行完毕,会阻塞所有写入column > 1数据的事务,此时并发效率是非常低的,如果锁住的数据量过大,都会导致一系列问题,在高并发的环境下是无法接受的,因为在MySql InnoDB中,默认是关闭间隙锁的,使用需要单独开启
innodb_locks_unsafe_for_binlog
快照读解决的问题
承接上述,在当前读带来的一些列问题,为了提高效率,MySql 引入了快照的概念;简述而言,在事务开始时,MySql 会给影响到的数据加一个版本号(trx_id),这个版本号即为当前事务id,当后续有其他事务对数据进行操作,版本号会递增,在前面的事务查询时,只会匹配到版本号等于或小于当前事务id的数据,因此无法读取到后续事务写入的数据,从而解决幻读,与不可重复读的问题;快照读的实现,主要依赖于版本链 & Undo & Read View
版本链
InnoDb会给数据加入三个隐藏列
// 事务标识,也可以理解为版本号
1.trx_id
// 回滚指针,指向这条记录的上一个版本
2.roll_pointer
// 当表不存在主键时,默认加入,用来标记数据是否删除,与本文关联不大,后续不再讲述
3.row_id
由trx_id 来标记数据的版本,即上一次事务的版本号;每次对数据进行改动,都会记录一条undo日志,每条undo日志都有一个roll_pointer属性(Insert操作对应的undo日志没有该属性,因为该数据并没有更早的版本);随着更新次数的增多,所有的版本都会被roll_pointer连接成一个链表,这个链表称为版本链,版本链的头节点就是当前记录最新的值;另外,每个版本中包含生成该版本对应的事务id(trx_id,即当前数据的版本号),在事务中判断版本可见性的时使用
Undo log
用于记录数据被修改之前的日志(即数据历史版本),在修改之前会先把数据写入undo log中,当事务进行回滚时可以通过undo log中的日志进行数据还原;undo log保证事务进行rollback时的原子性和一致性,事务进行回滚的时候使用undo log的数据进行恢复;在MVCC中,通过读取undo log的版本号(trx_id)实现不同事务读取自己可见的数据;undo log 主要分为两种
// 事务在insert时产生的undo log,只在事务回滚时需要,在事务提交后即可丢弃
1.insert undo log
// 事务在update & delete时产生的undo log,在事务回滚 & 快照读时需要
2.update undo log(主要)
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
记录并维护系统当前活跃事务的ID(trx_id)(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。
Read View
当事务执行快照读的时候,对该记录创建Read View(读视图),用它作为条件判断当前事务能看到的数据版本;可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据;即trx_id小于或等于当前事务id的数据
Read View的属性
// 当前系统活跃事务id集合(即未提交的事务)
1.trx_ids
// 创建当前Read View时,当前系统最新事务id+1,即新事务版本号比之前的大
2.low_limit_id
// 创建当前Read View时,未提交的事务最小的id
3.up_limit_id
// 创建当前Read View的事务id
4.creator_trx_id
可见判断
1.Read View中,数据版本号(trx_id)小于当前系统未提交事务的最小id,则说明该数据在当前所有未提交事务之前就已生成,故可见
2.Read View中,数据版本号(trx_id)等于当前事务id,则说明该数据由当前数据创建,故可见
3.Read View中,数据版本号(trx_id)小于当前事务id,且该数据版本号(trx_id)不存在于活跃事务id中(trx_ids),则说明操作该数据的事务已提交,故可见
4.Read View中,数据版本号(trx_id)小于当前事务id,且该数据版本号(trx_id)存在于活跃事务id中(trx_ids),则说明操作该数据的事务未提交,故不可见
5.Read View中,数据版本号(trx_id)大于当前事务id,则说明该数据是当前事务之后的事务创建的,故不可见
参考资料:
https://blog.csdn.net/SeekN/a...
https://baijiahao.baidu.com/s...
https://baike.baidu.com/item/...
https://blog.csdn.net/zuodaoy...