前言
2022/8/13 11:14
暑假学习ing
推荐
【MySQL数据库教程天花板,mysql安装到mysql高级,强!硬!-哔哩哔哩】
第16章 多版本并发控制
1. 什么是MVCC
MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC是通过数据行的多个版本管理来实现数据库的并发控制。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
MVCC没有正式的标准,在不同的DBMS中 MVCC的实现方式可能是不同的,也不是普遍使用的(大家可以参考相关的DBMS文档)。这里讲解InnoDB 中 MVCC的实现机制(MySQL其它的存储引擎并不支持它)。
2. 快照读与当前读
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突
,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
,而这个读指的就是快照读
,而非当前读
。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。
2.1 快照读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;比如这样:
SELECT * FROM player WHERE ...
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。
既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
2.2 当前读
当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁
3. 复习
3.1 再谈隔离级别
事务有 4 个隔离级别,可能存在三种并发问题:
在MySQL中,默认的隔离级别是可重复读,可以解决脏读和不可重复读的问题,如果仅从定义的角度来看,它并不能解决幻读问题。如果想要解决幻读问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但这样一来就会大幅降低数据库的事务并发能力
MVCC可以不采用锁机制,而是通过乐观锁的方式来解决不可重复读和幻读问题!它可以在大多数情况下替代行级锁,降低系统的开销。
3.2隐藏字段、Undo Log版本链
回顾一下undo日志的版本链,对于使用InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
trx_id
: 每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id
赋值给trx_id
隐藏列roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
举例: student表数据如下
SELECT * FROM student ; /* +----+--------+--------+ | id | name | class | +----+--------+--------+ | 1 | 张三 | 一班 | +----+--------+--------+ 1 row in set (0.07 sec) */
假设插入该记录的事务id
为8
,那么此刻该条记录的示意图如下所示:
insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。
假设之后两个事务id分别为10、20的事务对这条记录进行UPDATE 操作,操作流程如下:
发生时间顺序 | 事务10 | 事务20 |
1 | BEGIN; | |
2 | BEGIN; | |
3 | UPDATE student SET name=“李四” WHERE id=1; |
4 | UPDATE student SET name=“王五” WHERE id=1; | |
5 | COMMIT; | |
6 | UPDATE student SET name=“钱七” WHERE id=1; |
7 | UPDATE student SET name=“宋八” WHERE id=1; | |
8 | COMMIT; |
能不能在两个事务中交叉更新同一条记录呢?
不能!这不就是一个事务修改了另一个未提交事务修改过的数据,脏写。
InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:
update写的时候应该是默认加了x锁的,20会等待10
对该记录每次更新后,都会将旧值放到一条undo日志
中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表,把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。
每个版本中还包含生成该版本时对应的事务id
。