MySQL之MVCC浅浅理解~
前置知识
脏读、幻读、不可重复读
如现在有这样一个表
CREATE TABLE tableA (
id INT,
name VARCHAR(100),
age int,
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
表中插入了数据
INSERT INTO tableA (id, name, age) VALUES
(1, '张三', 20),
(2, '李四', 26),
(3, '王五', 18);
脏读
一个事务读取到了另一个事务未提交的数据。
事务1 | 事务2 |
---|---|
begin; | begin; |
select * from tableA where id = 1 结果为 1, 张三,20 | |
update tableA set name = ‘张三三’ where id = 1 | |
select * from tableA where id = 1 结果为 1,张三三,20 | |
rollback; |
不可重复读
一个事务读取到了另一个事务修改提交后的数据,即一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不同的~
事务1 | 事务2 |
---|---|
begin; | begin; |
select * from tableA where id = 1 结果为 1, 张三,20 | |
update tableA set name = ‘张三三’ where id = 1 | |
commit | |
select * from tableA where id = 1 结果为 1,张三三,20 |
幻读
一个事务先查询出一些记录,之后另一个事务又向表中插入了一些数据,原来的事务在按照原条件查询时,把另一个事务插入的数据也查询出来了。
事务1 | 事务2 |
---|---|
begin; | begin; |
select * from tableA where id > 1 结果为 2, 李四,26 与 3,王五,18 | |
insert into tableA values(4, ‘赵六’, 30) | |
commit; | |
select * from tableA where id = 1 结果为 2, 李四,26 ; 3,王五,18; 4,赵六,30 |
影响:脏读 > 不可重复读 > 幻读
Mysql的隔离级别
- READ UNCOMMITED,读未提交。会导致脏读、幻读以及不可重复读;
- READ COMMITED,读已提交。解决了脏读,但是会导致幻读以及不可重复读;
- REPEATABLE READ,可重复读。解决了脏读以及不可重复读,会导致幻读;
- SERIALIZABLE,序列化。隔离级别最高,解决了脏读、幻读以及不可重复读。
隔离水平高低排序:串行化 > 可重复读 > 读已提交 > 读未提交
MySQL默认隔离级别是可重复读~
而在Mysql中,是如何实现隔离级别的呢?Mysql中通过加锁的方式实现隔离级别,但加锁自然会带来性能上的损失,那么,如何解决性能问题呢?
MVCC多版本并发控制。
MVCC原理
概念
Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。
简单理解,如果读取的一行数据正在执行 DELETE 或者 UPDATE操作的话,这时读取操作不会因此等待行上锁的释放,相反的,InnoDB存储引擎会去读取行的一个快照数据,而所谓快照数据,是指该行的之前版本的数据,通过undo log日志实现。
因此可以看出,MVCC极大的提高了数据库的并发性。
MVCC只对READ COMMITED 与 REPEATABLE READ隔离级别实现~
版本链
首先我们要清楚,mysql中的表除了会记录我们自定义的列数据以外,Mysql还会为每个记录默认的添加一些列(隐藏列),具体的列:
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
DB_ROW_ID | 否 | 6字节 | 行ID,唯一标识一行记录 |
DB_TRX_ID | 是 | 6字节 | 事务ID |
DB_ROLL_PTR | 是 | 7字节 | 回滚指针 |
- DB_ROW_ID,唯一标识一行记录的作用,具体生成策略:
InnoDB存储引擎对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有自定义主键的话,则选取一个 唯一Unique键作为主键,如果表中连Unique键都没有的话,则InnoDB会为表默认添加一个名为 row_id的隐藏列作为主键。
- DB_TRX_ID:事务ID,每当一个事务对某条记录进行改动时,都会把该事务的事务ID赋给 DB_TRX_ID。即记录改动后,才会生成一个事务id;
- DB_ROLL_PTR:每次对某条记录进行改动时,都会吧旧的版本写入到 undo log日志中,然后DB_ROLL_PTR相当于一个指针,指向上一个版本的记录。
InnoDB存储引擎会为每条记录都添加 DB_TRX_ID 与 DB_ROLL_PTR这两列,而DB_ROW_ID则根据上述策略进行生成
如上表中最开始有这样一条数据
INSERT INTO tableA (id, name, age) VALUES (1, 'zs', 20);
且假设插入这条记录的事务ID (DB_TRX_ID) 为1的话,则该记录的版本链示意图
如果后续另一个事务2,修改了这条数据,如SQL
update tableA set name = 'zsss' where id = 1;
事务3修改这条记录,SQL
update tableA set age = 18 where id = 1;
从上可以看出来,每次对记录更新后,都会将旧值放到一条undo log日志中,即该记录的历史版本,随着更新次数增多,所有的版本都会被 DB_ROLL_PTR连接成为一个链表,称之为 版本链。版本链的头结点就是最新的数据,越往下越是老的数据~~
ReadView
通过 ReadView来判断版本链中的那个版本是当前事务可见的。
对于READ COMMITED 与 REPEATABLE READ来说,不同的隔离级别生成ReadView的时机也不同。
- Read Commited:每次进行快照读的时候都生成一份新的 ReadView;
- Repeatable Read:仅在快照读事务开始的时候生成一个ReadView,此后在同一个事务中的快照读都是用第一次生成的ReadView。
小贴士:
快照读:读取历史版本的数据,平常最常用的select语句即为快照读;select ....;
当前读:通过加锁的方式保证查询到的是最新的数据;
select ... for update; #对读取的行记录加上一个X锁; select ... lock in share mode; #对读取的行记录加上一个S锁; insert ....; update ...; delete ....;
ReadView到底是什么呢?
ReadView中包含了四个重要的内容:
-
m_ids:表示在生成ReadView时当前系统中活跃的读写事务事务id 列表。即当前系统中已开启事务但是还未提交的事务ID列表~~
-
min_trx_id:表示在生成ReadView时 m_ids中最小的事务ID;
-
max_trx_id:表示在生成ReadView时系统应该分配给下一个事务的ID。即当前系统中最大事务ID的下一个值~
-
creator_trx_id:生成该ReadView的事务ID。
小贴士:
只有在对表中的记录做改动时(执行INSERT、UPDATE等操作时)才会分配一个事务ID,在一个只读事务中的事务id值默认为0~
比较规则
因此,在进行快照读的时候,通过 ReadView与版本链进行对比即可判断版本链中的某个版本是否可见:
- 如果被访问版本的 DB_TRX_ID 事务id 与ReadView中的creator_trx_id相等的话,则意味着是当前事务在访问它自己修改过的记录,所以该版本是可见的;
- 如果被访问版本的DB_TRX_ID 事务id 小于 ReadView中的 min_trx_id,表明生成该版本的事务在当前事务生成ReadView之前已经提交,所以该版本可以被当前事务访问;
- 如果被访问版本的 DB_TRX_ID 事务id 大于ReadView中的 max_trx_id值 的话,表明生成该版本的事务在当前事务生成ReadView后再开启,所以该版本不可以被当前事务访问;
- 如果被访问版本的DB_TRX_ID 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 DB_TRX_ID 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃未提交的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
简单图示
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
看到这可能会有点迷,接着往下看看示例吧~~
Read Commited隔离级别下MVCC实现
Read Commited隔离级别下 每次进行快照查询时都会生成一个新的ReadView |
依然使用最上面的示例表~同样假设表中目前有这样一条记录,该记录由事务1创建
INSERT INTO tableA (id, name, age) VALUES (1, 'zs', 20)
目前该条记录的版本链
两个事务如下表执行:
事务2 | 事务3 |
---|---|
begin; | begin; |
① select * from tableA where id = 1; | |
update tableA set name = ‘zss’ where id = 1 | |
②select * from tableA where id = 1; | |
commit; | |
③select * from tableA where id = 1; |
①:事务2执行第一个select语句时,生成一个ReadView,因为该事务未对表中的记录做更改,且事务2与3均未提交事务,所以 creator_trx_id = 0,m_ids = [2, 3], min_trx_id = 2, max_trx_id = 4。
通过该ReadView与上述的版本链进行对比,DB_TRX_ID = 1 小于 ReadView中的min_trx_id = 2,所以该查询语句能够读取到这条记录,因此返回的结果为 (1,‘zs’,20)
②:事务2执行第二个select语句时,新生成一个ReadView,creator_trx_id = 0,m_ids = [2,3],min_trx_id = 2, max_trx_id = 4。
但是事务3修改了数据,因此记录版本链为
通过ReadView与版本链进行对比,DB_TRX_ID = 3 在 ReadView的min_trx_id = 2 与 max_trx_id = 4之间,并且DB_TRX_ID = 3在m_ids数组中,该版本的事务还未提交,因此读取不到该版本的数据~
则从版本链中判断下一个版本的数据,DB_TRX_ID = 1 小于 ReadView中的 min_trx_id = 2,该版本的事务已经提交,因此可以读取到版本的数据;
所以该查询语句最终返回的结果为 (1,‘zs’,20).
③事务2执行第三个select语句时,因为事务3已经提交了,因此新生成的ReadView:creator_trx_id = 0,m_ids = [2],min_trx_id = 2,max_trx_id = 4
该条记录没有变动,因此记录版本链和上面相同
对比版本链与 ReadView,首先 DB_TRX_ID = 3 在 ReadView中 min_trx_id = 2 与max_trx_id = 4中间,但是 DB_TRX_ID =3不在 m_ids数组当中,代表该版本的事务已经提交了,因此能够访问到该版本的数据。所以该查询语句的返回结果为 (1,‘zss’,20)
这也就是 读已提交Read Commited隔离级别下产生 不可重复读问题的原因~
Repeatable Read隔离级别下MVCC的实现
Repeatable Read隔离级别下只会在第一次快照读的时候生成一份ReadView~ |
仍然是上面的表与同样的两个事务执行;
①事务2执行第一个select语句时,生成的ReadView,因为该事务未对表中的记录做更改,所以 creator_trx_id = 0,m_ids = [2, 3], min_trx_id = 2, max_trx_id = 4。
目前的版本链
对比ReadView与版本链,DB_TRX_ID小于 ReadView中的 min_trx_id = 2,因此返回的数据为 (1,‘zs’,20)
②事务2执行第二个select语句时,不会再新生成ReadView,可重复读隔离级别下只会在第一次快照读的时候生成ReadView,因此ReadView为creator_trx_id = 0,m_ids = [2, 3], min_trx_id = 2, max_trx_id = 4
此时的版本链为:
对比ReadView与版本链,DB_TRX_ID=3在ReadView的 min_trx_id = 2 与max_trx_id=4之间,且DB_TRX_ID=3在m_ids数组当中,说明该版本的事务还未提交,因此读取不到该版本的数据,转而对比版本链中的下一个版本;
下一个版本中的DB_TRX_ID=1,小于ReadView中的 min_trx_id=2,因此该版本的数据可以读取到,因此最终返回的数据为 (1,‘zs’,20)
③事务2执行第三个select语句时,因为ReadView和版本链均未变化,因此和第二个select语句执行流程相同,返回的结果也是相同的。
从上流程也可以看出,可重复读Repeatable Read 解决了不可重复读的问题~~~而至于幻读呢,可重复读隔离级别下其实是通过 next-key lock解决的,此处就不过多介绍啦,需要的小伙伴可以下来自行了解哈~
以上就是我对MVVC的浅浅理解啦~
参考:《MySQL是怎样运行的:从根儿上理解MySQL》