1.什么是MVCC?
MVCC全称是【Multi-Version ConCurrency Control】,即多版本控制协议。
多版本控制(Multiversion Concurrency Control): 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,不需要MVCC的支持;Serializable 则会对所有读取的记录行都加锁,单靠MVCC无法完成。
MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log。
2.MVCC解决了什么问题?
在并发事务下,可能会产生如下问题:
1.脏读 :当前事务读取到其它事务未提交的数据。
2.脏写 : 事务B提交后,将事务A提交的数据覆盖。
3.不可重复读:在同一个事务中,不同时间段执行相同的查询语句,得到的结果集不相同。
4.幻读:事务A读取到了事务B新增的数据。
MVCC可重复读模式下,解决了事务的脏读、脏写、不可重复读等问题,但是还是存在幻读问题,幻读问题可以使用间隙锁进行解决。
3.MVCC的实现原理?
什么是行锁、表锁、间隙锁?
首先锁的存在,目的是为了在并发场景下,保持数据的安全、一致。
并发场景有:
读-读 :此并发场景不需要进行并发控制,也就是不需要加锁。
读-写 :此并发场景需要并发控制,不然就会出现脏读,幻读,不可重复读的问题。
写-写 :此并发场景需要并发控制,不然就会出现更新丢失的问题。
进行并发控制,常规手段就是加锁,其中mysql的锁有以下几种:
行锁:锁住表中的一行;比如 update user set name=‘张三’ where id=1;会锁住id=1的那一行数据,其他事务再想更新,就只能等前一个事务释放锁。
表锁:锁住整个表,比如update user set name=‘张三’;由于没有加where条件,此更新sql会对整个表进行更新,也就是会锁住整个表。
间隙锁:比如事务A执行update user set name=‘张三’ where id >1 and id<4; 假如表中只有id=1、2 两条数据,A事务还没提交,那么此时事务B再次插入一条id=3的数据,理论上是允许的,但是实际上是B只能等A提交,因为事务A执行的是id>1and id<4,范围涵盖了id=3的,也即是把id=3的这个间隙也给锁了,叫做间隙锁。
除了这3种锁,还有乐观锁、悲观锁、记录锁、自增锁、意向锁;
既然可以使用行锁、表锁、间隙锁来保证数据操作的安全性,那么还要MVCC的出现是为何呢? 实际是因为在性能方面还有优化的空间。
虽然使用锁可以保证数据安全,但是毕竟加了锁就意味着并发性能的降低,因此,能不使用锁就尽量不使用锁。在某些场景下,MVCC可以在比使用锁更快。
在读-读、读-写、写-写这3种并发场景中,读-写 可以不使用锁,而是使用MVCC来实现数据的并发操作以及安全一致性。
因此,mysql是同时使用了MVCC+行锁、表锁、间隙锁来保证了数据安全,又尽可能大的实现了性能最优化。
多版本
那么不使用锁,MVCC是如何更高效的解决读-写这种并发场景下的数据安全呢?
我们知道,事务在执行失败时,会将数据回滚为上个版本,而MVCC叫做多版本并发控制,核心概念就在版本上,也就是说数据库存储了多个版本的数据。
多个版本整体上分为两类:最新版本、历史版本。
这也牵扯出来另外两个概念:
快照读:读取的是数据库种历史版本的数据;
常规的select * from user ;属于是快照读;
当前读:读取的是数据库种最新版本的数据;当前读的操作有:
select * from user in share mode(共享锁),;
select * from user for update;
update, insert ,delete(排他锁)
那么多个版本,mysql是如何存储的呢?
innodb存储引擎中,我们存在表中的数据,除了我们设置的业务字段,另外还有3个默认字段,如下图末尾3个:
总结
MVCC:
1、MVCC在不加锁的情况下,解决了并发事务的脏读、脏写不可重复度等问题,而幻读可以使用间隙锁进行解决。
2、undo log里面通过两个隐藏字段trx_id、roll_pointer将历史快照数据串联起来,形成一个版本链,是read view获取数据的前提。
3、read view是在第一次查询时生成的,由所有未提交的活跃事务id组成的数组和最大事务id构成。
4、通过对比事务id的大小,将数据进行展示。