MVCC
什么是MVCC?
MVCC(Multiy Version Concurrent Control),即多版本并发控制,是一种乐观锁的实现。
MVCC使得读不会加锁,提高了数据库的并发处理能力。通过MVCC,MySQL可以实现【读已提交】和【可重复读】等隔离级别,保证了隔离性。
MVCC原理:
数据库中同时存在多个版本的数据,即同一条记录数据临时保存多个版本的一种方式, 是通过undo log来实现的。
不同的事务可以读取到不同版本的数据,从而解决脏读和不可重复读的问题
即在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事务隔离级别去判断读取哪个版本的数据
即根据事务开始的时间不同,不同的事务对同一张表,同一时刻看到的数据可能是不一样的。但是对每个事务来说,在它的执行期间,不管它执行多长时间,它看到的数据都是一致的。
InnDB中的MVCC:
1、事务版本号
InnoDB中每个事务都有一个唯一的事务ID,即为transaction_id。
事务每次开启前,都会从数据库引擎申请一个自增长的事务ID(按照时间先后严格递增),可以通过事务ID判断事务的执行先后顺序。
2、隐藏字段
对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id、roll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id
列名 | 是否必须 | 描述 |
---|---|---|
row_id | 否 | 单调递增的行ID,不是必需的,占用6个字节。 |
trx_id | 是 | 记录操作该数据事务的事务ID |
roll_pointer | 是 | 这个隐藏列就相当于一个指针,指向回滚段的undo日志 |
每次事务更新数据就会生成一个新的数据版本,并把transaction_id记为 trx_id,同时,旧的数据版本会保留在undo log中,而且新的版本会记录旧版本的回滚指针,通过它直接拿到上一个版本。
所以,InnoDB中的MVCC其实是通过在每行记录后面保留两个隐藏的列来实现的。一列是事务ID:trx_id ; 另一列是回滚指针:roll_pointer 。
3、undo log :回滚日志
每行数据有多个版本,是依赖undo log 来实现的
undo log记录事务修改之前的数据的一个版本(记录数据被修改前的信息),在表记录被修改之前,会先把数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据。同时提供MVCC下的快照读。
因此undo log的作用是:
1、事务回滚时,保证原子性和一致性
2、用于MVCC快照读
根据操作的不同,undo log分为两种:insert undo log 和 update undo log。
1) insert undo log
insert 操作产生的undo log,因为insert 操作记录没有历史版本只对当前事务本身可见,对于其他事务此记录不可见。所以insert undo log可以在事务提交后直接删除而不需要进行purge操作。
purge:主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收 undo pages。
2)update undo log
update和delete删除产生的undo log都属于同一类型,update可以视为insert 新数据到原位置,delete旧数据,undo log 暂时保留旧数据。
多个事务都更新某条记录的时候,都会把旧的记录数据保存到undo log日志中。因此undo log日志中会存在同一条数据的多条记录的的情况。这也就是同一条记录在数据库中存在多个版本,也就是MVCC。
4、版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。
如:一个事务对表中的一条记录(trx_id=100)进行修改,那么操作流程如下:
1、首先获取一个事务ID,如101
2、把表中的修改前的记录,拷贝到undo log中
3、再修改表中的记录,
4、把修改后记录的隐藏字段trx_id改为当前事务版本号101,并把roll_pointer指向undo log数据地址
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突 ,做到即使有读写冲突时,也能做到不加锁 , 非阻塞并发读,而这个读指的就是快照读 , 而非当前读
5、快照读和当前读
事务的更新操作,是先读后写,这个读是查询的数据行的最新数据。而在事务中的查询语句的读,是从历史版本中查询的,叫做快照读。
1、快照读
查询当前事务可见的版本中的记录数据,也就是利用MVCC机制读取可见版本快照中的数据
不加锁的普通的select语句都是快照读。比如:SELECT * FROM user WHERE ...
①:快照读是基于MVCC实现的,提高了并发的性能,降低开销
②:大部分业务代码中的读取都属于快照读
2、当前读
读取记录数据的最新版本
读取时会对读取的记录进行加锁, 其他事务就有可能阻塞
加锁的 SELECT,或者对数据进行增删改都会进行当前读
事务对记录更新操作,读取到的是表中数据的最新值,然后执行更新操作,因为不可能去更新历史版本中的数据,可能已有其他事务先更新了。
select当前读
除了查询语句外,select语句如果加锁也是当前读。
加锁方式如:lock in mode 或 for undate
如:
select age from t where id=2 lock in mode.
select age from t where id=2 for update.
6、read-view(视图)
read-view是InnoDB在实现MVCC时用到的一致性视图,用于支持【读已提交】以及 【可重复读】隔离级别的实现。
read-view 不是真实存在的,只是一个概念,undo log才是它的体现。它主要是通过版本和undo log计算出来的,作用是决定事务能看到哪些数据。
读已提交和可重复读生成read-view的时机不同:
读已提交:每次读取数据前都生成一个read-view
可重复读:在第一次读取时生成一个read-view,后面查询不会创建新的read-view
数据版本的可见性规则:
read-view 中主要包含当前系统中还有哪些活跃(未提交)的读写事务ID,它的数据结构是一个数组。
在实现上InnoDB为每个事务构造了一个read-view数组,用来保存事务(读取数据前:读已提交;第一次读取数据前:可重复读)当前正在活跃(还未提交)的读写事务ID
说明:
min_trx_id:在生成read-view时,当前系统中活跃的读写事务中最小的事务ID
creator_trx_id:创建当前read-view的事务ID
max_trx_id:在生成read-view时,系统中应该分配给下一个事务的ID值,也就是全局事务中最大的事务 id 值 + 1
trx_ids:在生成read-view时,当前系统中活跃(未提交)的事务IDS数组, “活跃事务”指的是,启动了但还没提交的事务
每个事务或语句有自己的一致性视图,普通查询语句是一致性读,一致性读会根据row trx_id和一致性视图确定数据版本的可见性。
规则如下:
①:如果数据记录的事务ID,trx_id < min_trx_id,表明生成该记录版本的事务在生成当前read-view前已经提交了,所以该版本可以被当前事务访问。
②:如果trx_id > max_trx_id,表明生成该记录版本的事务在生成当前read-view后才创建,所以该版本不可以被当前事务访问。
③:如果 min_trx_id =<trx_id< max_trx_id
(1):如果trx_ids包含trx_id,则代表当前read-view生成时,这个事务还未提交,但是如果数据的trx_id=creator_trx_id的话,表明数据是自己生成的,因此是可见的,如果trx_id不等于creator_trx_id,则不可以被当前事务访问
(2):如果trx_ids不包含trx_id,则表明这个事务在read-view生成之前就已经提交了,修改的结果,当前事务是能看见的(在读已提交隔离级别中才有这种情况)
问题:当一个新的事务开启后,一个比当前事务id小的事务进行了数据修改提交,那么,当前事务可以读取到比自己事务id小的事务的修改后的最新数据吗?
答案:可以
总结:查询一条记录,基于MVCC的流程
1、生成当前事务,并获取当前事务ID
2、生成read-view
3、查询得到的数据,然后和read-view中的事务版本号进行比较
4、如果不符合read-view的可见性规则,就需要从undo log中获取历史快照数据,同样进行可见性规则校验
5、最后返回符合规则的数据
因此,InnoDB实现MVCC,是通过read-view + undo log实现的,undo log中保存了历史数据快照,read-view的可见性规则帮助判断当前版本的数据是否可见。
什么是幻读?
指的是当某个事务在读取某个范围内的记录时,会产生幻行,即两次读取的数据行不一致。
原因是,一个事务先读取了某个范围的数量,同时,另一个事务新增了这个范围的数据,再次读取发现两次读取的结果不一致。
MVCC使用快照读解决了部分幻读问题,但是在修改时还会存在幻读问题(如,当前事务在对记录进行范围修改时,这时其他事务在范围内插入了一条记录,根据read-view可见性规则,这条记录对当前事务是不可见的。但是这条新记录id在当前事务修改的范围内,由于修改走的是当前读,因此在修改后,这条新记录又会对当前事务可见,当前事务再次范围查询时就会出现幻读),幻读最终是通过间隙锁解决的。
什么是间隙锁?
间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读问题。
间隙锁(GAP LOCK):当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP),InnoDB也会对这个间隙加锁,这种锁机制叫做间隙锁。
如:user表中存在id为1,3,4,5,6的5条记录,那么这个SQL:
update user set score=’x‘ where id<6;
这条sql使用的是范围作为条件,InnoDB分为会为这条sql加排他锁。不仅对符合条件的记录加了锁,对不存在的记录,id为2的间隙也会加锁。这时候如果插入一条id为2的记录就会阻塞。这就是间隙锁。
END.