2024小结:在写作分享上,这里特别感谢阿里云开发者社区提供平台,支持大家持续学习分享交流,共同进步。社区诚意满满的干货,让大家收获满满。
对我而言,珍惜每一篇投稿分享,每一篇内容字数大概6000字左右,加上画图,以及案例demo代码编写、实战,撰稿时长平均3小时左右。由于年底工作特别忙,晚上下班回家,有时候娃已经睡着了,如果娃没睡还得陪娃玩直到她睡着才有空继续写作。每天空闲时间非常少,经常一篇文章从周一写到周末才能完成。
近5个月以来投稿并不多,仅29篇。好在其中超过一半,多达有17篇得到平台认可,认定为高分内容。2014是收获的一年。感恩感谢!新的一年,争取有更多时间,和大家交流学习分享,包括家庭、日常、职场其他非技术性内容。
一、前言背景
二、通俗演义-MVCC多版本并发控制核心原理
2.1 解密-基于undoLog实现的数据版本链
2.2 弯弯绕绕看不懂的readView视图-一句话总结看懂
三、MVCC解决脏读、不可重复读、幻读问题demo详解
3.1 验证MVCC解决脏读、不可重复读问题【并发事务一个重复查+另一个改】
3.2 验证MVCC解决幻读、不可重复读问题【并发事务一个重复查,一个新增】
四、脏写是什么?如何解决
期待可以写一篇2024总结,聊聊日常生活、职场等非技术内容。
一、前言背景
之前系列4文章说过,MySQL InnoDB存储引擎,默认事务隔离级别是可重复读repeatable-read。我们可通过命令查看:SELECT @@SESSION.tx_isolation;
而且也说到,MySQL的可重复读事务隔离级别,可以解决脏读、幻读、不可重复读三大事务并发问题。当时也留了一个思考题:MySQL是如何做到的?答案是MVCC+锁。核心在于MVCC。
那MySQL如何让实现一个事务多次读,不受另一个事务的增、改、删的影响。带着这个问题,我们一步步解密MySQL的MVCC多版本并发控制核心机制。
二、通俗演义-MVCC多版本并发控制核心原理
MVCC,全称是Multi Version Concurrency Control多版本并发控制。MySQL innoDB存储引擎,在新增修改删除数据的时候,并没有真正用新数据直接更新覆盖,而是采用版本链方式去保存数据修改记录。每个读事务,对应一个版本的数据快照。每个写事务,在事务提交之前,该事务内做的任何更新操作未提交之前,其他任何事务不可见该更新。
举一个通俗的案例,也是我们日常实践的数据版本管理。比如用户信息user (id,name,age,city,cs_level)修改,我们不会简单的直接进行update,通常会进行数据历史记录。比如最简单的user表增加一个is_valid字段,利用主键id自增的特性,把它当做用户信息更新版本号。每次更新用户信息,将原信息is_valid置为false。然后用新信息去构建一条is_valid=true数据。这样就可以完成版本记录追溯。
MVCC的数据快照、数据版本链就是是类似效果。但是在并发事务里读写,具体原理会复杂很多。
2.1 解密-基于undoLog实现的数据版本链
在MySQL表里,有2个隐藏的字段,一个是事务的ID:trx_id,这个事务id就是最近一次更新该数据的事务id;另一个是回滚指针:roll_pointer ,该指针指向的就是更新该数据之前的undoLog,可以通俗理解为:修改前数据。据此,隐藏的事务id,和回滚指针的意义,一目了然,一个表示哪个事务更新了该数据,一个表示该数据更新前的样子。
比如下图:事务trx_id=98的操作,新增了name=【拉丁解牛说技术】的这行数据。新增数据的时候,roll_pointer是空。此后,事务99对该数据进行修改,把name改为=【老牛】。此时如下图,回滚指针指向老版本事务98的数据。这样数据版本链清晰可见。
另外说一下,在MVCC里只有更新、删除、新增操作有让事务ID新增,查询是不会让数据事务发生变化。
2.2 弯弯绕绕看不懂的readView视图-一句话总结看懂
readview顾名思义是读视图,当开启一个事务,MySQL会根据当前事务隔离级别,给你这个事务开启独立的review视图空间。这里很多博文在讲解readview机制时候,会对当前最大max_trx_id最小,min_trx_id,当前事务this_trx_id,视图开启时候活跃的事务id组等多个事务id进行比较说明,讲的非常细。不过这里的判断规则说这么细,如果读者些微没跟上,或者失去耐心,可能就错失理解掌握readview的核心机制。
我们坚持大道至简的方法,总结readview视图核心机制,最直接一句话:每个事务只能读到对应事务隔离级别的数据。
我们简单举例说一下:
如下图,当前事务隔离级别是可重复读RP,并发事务100要重复多次查询,事务101 要更新name为【zhangsan】。事务并发开始前如下:
接下来具体操作:
1、事务id=100进行查询,首先查到了事务99的数据,发现自己的事务ID100比99大,直接返回读到该数据【老牛】。
2、接下来事务id=101,把数据更新为【zhangsan】并提交更新trx_id=101,回滚指针指向了之前trx_id=99的老数据。
3、事务100,继续重复读,这时候读到了trx_id=101的最新数据,发现比自己的trx_id=100还大。在当前每个事务只能读到对应事务隔离级别的数据原则下,而且当前事务隔离级别是可重复读RP。按规则,不好意思,这个101事务修改的数据我不能读,得继续遍历undoLog版本链,找到了下一个事务id=99的数据【老牛】,发现99< 100,非常好,符合事务隔离级别要求。那这次重复读,读的还是之前的数据【老牛】。
同样道理,如果是事务隔离级别在读已提交、读未提交,判断规则也只是对应判断当前自己的事务ID与读到的数据事务id大小关系是否满足事务隔离级别即可。
当然,这里核心再详细展开确实有很多细节,比如读已提交隔离级别下,每次查询,就是开启一个新的readview。这个和可重复读隔离级别不一样。
理解了MVCC核心原理,我们设计场景,在InnoDB默认的事务隔离级别「可重复读RP」下,一步步实践验证解决脏读、幻读、不可重复读问题。
三、MVCC解决脏读、不可重复读、幻读问题demo详解
新建一个user_mvcc_demo表,多个事务并发读、修改name值、以及新增写入,来具体验证mvcc核心机制。
CREATE TABLE user_mvcc_demo ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(16) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -- 新增一条数据,id=1 insert into user_mvcc_demo(name)values ('拉丁解牛说技术001');
3.1 验证MVCC解决脏读、不可重复读问题【并发事务一个重复查+另一个改】
之前说过:脏读,特指的就是一个事务里select查询到另一个事务update语句未提交的脏数据场景。
本demo模拟场景:事务1里面多次重复查询id=1的name值,而事务2并发修改了name值并提交。
预期结果:在事务1里每次查询都是数据快照‘拉丁解牛说技术001’;事务2提交事务前、后,事务1均读不到name的新值zhangsan。
具体如下:
事务1 SQL:
begin;
select * from user_mvcc_demo where id=1;
select now();-- 时间 2025-01-03 16:04:46
select * from user_mvcc_demo where id=1;
多次查询
.....
事务2 SQL:
begin;
select * from user_mvcc_demo where id=1;
select now();-- 时间 2025-01-03 16:04:46
update user_mvcc_demo set='zhangsan' where id=1;-- 更新name 为zhangsan
select * from user_mvcc_demo where id=1;
select * from user_mvcc_demo where id=1;--- 此时未提交,但事务1读不到脏数据zhangsan
commit;--提交后,事务1仍然读不到新快照数据zhangsan
.....
实践结果:
在事务1内,多次查结果都是:拉丁解牛说技术001。实际上,在另一个事务2, 时间 2025-01-03 16:04:26 已经修改name为zhangsan。
且事务2提交事务后,在事务1里,继续多次查询,查到的也是事务1开启事务后,对应的快照数据:拉丁解牛说技术001,验证MVCC解决脏读、不看重复读问题完成。
如下图两个会话:
3.2 验证MVCC解决幻读、不可重复读问题【并发事务一个重复查,一个新增】
系列3具体说过,不可重复读问题:一个事务读到了另一个事务已提交的数据。主要针对的是一个事务里select到另一个事务update或者delete语句的更新结果。
而幻读:一个事务读到另一个事务新增的数据,特指一个事务里select查询查到了另一个事务insert数据。
本demo模拟场景:事务1里面多次重复查询全表,而事务2新增了一条数据并提交。
预期结果:在事务1里每次查询都只有一条数据;事务2提交事务前、后,事务1均读不到新增那条数据。
仍然是user_mvcc_demo表,里面只有一条id=1,name=‘zhangsan’;的数据。
事务1:重复读select * from user_mvcc_demo 。
事务2:新增1条数据。insert into user_mvcc_demo(name)values('拉丁解牛说技术');
具体如下:
事务1 SQL:
begin;
select * from user_mvcc_demo;
select now();-- 时间 2025-01-03 16:40:45
select * from user_mvcc_demo;
多次查询
.....
事务2 SQL:
begin;
select * from user_mvcc_demo;
select now();-- 时间 2025-01-03 16:40:22
insert into user_mvcc_demo(name)values('拉丁解牛说技术');;-- 新增了一条'拉丁解牛说技术'的数据
select * from user_mvcc_demo;
commit;--提交后,事务1仍然读不到新快照数据'拉丁解牛说技术'
.....
实践结果,与预期一致:在事务1里每次查询都只有一条数据zhangsan;而事务2提交事务前、后,事务1均读不到新增那条数据'拉丁解牛说技术'。
四、脏写是什么?如何解决
脏读,说过很多次,脏写大家听的很少。所谓脏写:就是并发事务更新同一个数据,比如2个并发事务修改id=1,的name值,之前name值是【拉丁解牛说技术】。事务1此时想改成zhansan并提交,而事务2改成lisi,但是中途回滚了,回滚为老数据【拉丁解牛说技术】。对于事务1来说,这就是脏写问题。
MySQL是通过锁机制来保障并发事务串行化执行,避免事务并发脏写问题。简单的说,更新事务读到数据后,需要先加锁,加锁成功才能开始执行更新事务。未拿到锁的事务,需要等待锁。这个和java并发编程的锁机制类似。
篇幅有限,具体锁相关类型、以及具体场景锁分析,我们下一篇继续分享。
推荐阅读拉丁解牛相关专题系列(欢迎交流讨论,搜索:拉丁解牛):