mysql事务隔离级别以及MVCC的底层原理
有猿友私信聊天说搞不明白事务隔离级别到底是什么意思,但面试又不可避免,只能死记硬背。但资深的面试官不讲武德,直接询问事务隔离级别底层原理,怎么办?来骗?来忽悠经验丰富的面试官?这好吗?这不好。我劝这些猿友,耗子尾汁,脚踏实地研究技术,不要搞窝里斗。
本文帝都的雁为大家详细介绍一下mysql底层保证数据安全的原理。
一、锁的分类
万变不离其宗,锁是一个抽象的概念,按照不同的角度有不同的划分。
1、以性能划分
乐观锁:当线程获取不到锁时,会进行自旋重试,不会阻塞用户线程,但牺牲cpu的资源。
悲观锁:当线程获取不到锁时,阻塞用户线程,直到锁释放,重新获取。阻塞线程会造成计算机从用户空间切换至内核空间再切换回用户空间这种耗时操作,故效率较低。
2、以粒度划分
共享锁(读锁):只对数据进行读取。多个共享锁可以共存。
排它锁(写锁):对数据进行修改。不允许其它锁共同访问数据。
重入锁:同一线程对锁多次访问,不会多次加锁,而是进行重入,减少加锁解锁的时间开销。
公平锁:多个线程获取一把锁,获取不到的线程排队等待获取。
非公平锁:多个线程获取一把锁,锁释放时,抢夺锁的线程不一定是按照访问顺序而来的线程。
3、以数据库划分
表锁:对整张表进行锁定,表不存在事务。
行锁:对指定行进行加锁,存在事务。
二、存储引擎
以myisam和innodb为例
1、Myisam
只能进行表锁,无事务。
-- 加锁
Lock table read(write);
-- 解锁
Unlock tables;
当session1(mysql的connection)对table1进行表锁read时,只能对table1查询,不能做修改,也不能对其它表做任何操作;同时其它session可以访问table1的数据,但不能做修改。
当session1对table1进行表锁write时,只能对table1表做更新操作,不能做查询,也不能对其它表进行任何操作;同时其它session不能访问table1。
2、Innobd
支持行锁,存在事务
Innodb在执行insert/update/delete语句时,会默认帮我们开启事务。
即在sql执行前进行begin操作,执行结束进行commit/rollback。
我们也可以手动进行begin-commit/rollback事务控制。
但要注意,并非在begin之后马上就开启事务,而是begin之后的下一条sql语句执行时,mysql找到这条sql对应记录的行,对行进行加锁。所以行锁和事务是紧密相连的。
三、事务隔离级别
为了通俗易懂,我们举个例子。
用户表中有三个字段:ID,NAME,AGE。其中ID为主键,name加了索引。
1、读未提交(READ UNCOMMITTED)
此隔离级别下,session1进行了一个操作:
Begin;
Insert into user(id,name,age)values(1,’帝都的雁’,27);
此时session2有个需求,需要统计用户个数,于是:
Begin;
Select count(*) from user;
Commit;
拿到结果后进行业务处理,美滋滋。但这是session1进行rollback,显然session2拿到了一条不存在的数据进行处理,造成一系列问题。很快,很快啊,被客户投诉。
这就是读未提交造成的脏读现象。
一个事务读取到了另一个事务为提交的数据。
2、读已提交(READ COMMITTED)
上一个案例大意了啊,没有闪。
这次session2学聪明了,将它的事务隔离级别改为读已提交。然后重复场景,但这次有些不同,session2心急如焚先查询了一次,结果为0;然后session1插入数据并提交;session2有了投诉的阴影,为了确保正确,又查询了一次,发现结果变成了1,心态炸裂有木有?到底以哪个为准?赶快找老大分析,老大看都不看,轻描淡写地吐出:“不可重复读”。
一个事务读到另一个事务提交的数据。
3、可重复度(REPEATABLE READ)
老大给出建议,不要修改session的事务隔离级别,使用默认的可重复读即可。
即一个事务开启之后,只要自己事务内不对数据做变动,那么在事务结束之前,不管查询多少次,结果都是相同的(默认存在一个视图,将结果集缓存)。
Session2欣喜若狂,开启事务后,开始查询数据,然后进行数据更新:
Begin;
Select * from user;
Update user set name= ‘帝都的雁’ where age = 18;
Select * from user;
Commit;
session2在执行完第一条select时,发现表中有20条数据;在它执行update前,无独有偶,session1又来搞破坏了。Session1插入了一条数据,其age=18(你说巧不巧),session1提交事务后,session2此时还不知道发生了什么,开始执行update操作,update返回影响的行数,好像不是预期的数量,session2心里咯噔一下,又战战兢兢地地执行了一次查询,发现结果集多了一条数据!王德发?Session2开始质疑老大:这次隔离记录也修改了,为什么还是不可重复度啊,我出现幻觉了吗?老大对session1一顿询问,得知session1干的好事,淡淡一笑:这是幻读。
幻读,事务A在进行数据变动时,别的事务的数据突然新增或变更成为事务A数据变动的where条件的匹配记录行,导致事务A处理数据时把这条不速之客也一并处理了。
解决幻读的方式,可以采用串行化。
4、串行化(serializable)
Session2痛定思痛,既然大家不讲武德,那直接放大招,将事务隔离级别改为串行化。
老大赶忙阻拦。串行化,可以解决这些问题,但它把mysql变成了单线程,一点并发不给留。年轻人,耗子尾汁啊。
四、MVCC
Innodb引擎默认session的事务隔离级别为可重复读。那innodb是如何做到不同事务之间的可重复读呢?
其实,面对一份数据,innodb会为其准备多个版本,用于不同的事务,每个事务在开启时,都会有自己对应的事务版本(快照),每个版本对应各自事务开启时刻的数据。
简单来说,新创建一张表,innodb默认帮我们附加了三列。
db_row_id:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键或唯一索引,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。
db_trx_id: 该记录进行事务的id,新增/修改都会触发。
db_roll_ptr:指向当前记录如果发生事务回滚,要回滚至哪一个版本数据的地址。
当我们变更(update/delete)表中数据时,session会先获取到全局的事务id,然后将事务id放入到这些记录行的db_trx_id中,当执行commit时,全局事务ID自增。而不同版本的数据,以链表的方式存放在innodb的undo.log中,版本号大的在上面。
执行insert操作时,db_roll_ptr为空。显然插入失败,不需要回退版本。
举个例子:
假设一张空表user(id,name,age)
Begin;
Insert into user(name,age)values(‘帝都的雁’,27);
Commit;
在执行insert前拿到全局事务id为0,那么当执行commit时,全局事务ID为1;当执行rollback时,事务ID为0。整个过程中,这条记录的db_roll_ptr始终为空。
再执行一次上述操作,也同样如此。
执行一次update。
Begin;
Update user set age =18 where id =1;
Commit;
那么update执行时,会先把记录查询出放到buffer poll中,进而放入到undolog,记录中的事务ID为1,db_roll_ptr指向undolog中数据的位置(假设为1)。
事务commit后,全局事务ID自增;事务回滚后,innodb根据这行数据的db_roll_ptr执行的undolog位置1处的数据,进行版本回退。
再执行一次update操作时,同理,只不过原数据放入到undolog日志位置2处,而2中行数据的回滚地址执行1,以链表的方式存放不同版本的数据快照。
Delete与update同理。
现在我们分析一下为什么MVCC可以实现可重复读。
当我们session进行begin时,拿到了当前全局事务ID(假设为100)。那么我们查询数据时,所有的数据行的事务ID都不能超过此事务ID。在此期间,有其它session事务提交,导致全局事务ID自增。但第一个session在没有提交前,其获取的事务ID不会变,即始终访问它开启事务那个时刻的数据,所以可以实现可重复读。
其实事务开启后,首次查询会把结果放到一个视图中,只要事务中没有涉及视图数据的变更,下次查询就会访问视图数据。
五、间隙锁
首先要明确一个概念,所谓行锁,并不是将这行记录锁定,而是将记录对应的索引锁定。即上锁,是对索引上锁。
索引又是按照顺序排列存放,所谓间隙锁,即索引区间的锁定。
举个例子:
Update user set age = 18 where id > 18 and id < 50;
那么,在执行sql时,主键索引(18,50)区间的索引值都会被锁定,就算可能当前id最大为30,但30~50的索引值还是会被索引。此时其它事务想要insert一条ID=31的数据,会发现阻塞,等待update的commit /rollback。
并非主键索引/唯一索引一定会使用间隙锁。当where条件命中具体行数据时,使用记录的行锁;当where条件命中一个区间时,才会使用间隙锁。
由于锁是针对索引,故如果加锁时发现索引失效,那就很可能全部扫描,每个记录行都加锁,导致行锁升级为表锁。
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁