RDB | MySQL的事务隔离级别和锁的机制
对于后端开发面试,关系型数据库是一个必问考点。而在目前主流的关系型数据库中,MySQL无疑是互联网圈中最火热的一个。掌握MySQL的核心知识点,对于面试取得一个好成绩很重要!
我们都知道目前MySQL的事务隔离级别主要有四个,分别是:读未提交、读已提交、可重复读 和 串行化,如下图所示:
按隔离水平高低排序:读未提交<读已提交<可重复读<串行化。
(1)读未提交:读未提交:隔离级别最低,隔离度最弱,理论存在,实际中几乎没有人用,但性能最高。该级别存在的问题包括 脏读、不可重复读 和 幻读。
(2)读已提交:保证了事务不出现中间状态的数据,所有数据都是已提交且更新的。该级别存在的问题有 不可重复读 和 幻读。
(3)可重复读:MySQL InnoDB引擎的默认隔离级别,保证了一个事务中多次读取数据的一致性。该级别存在的问题有 幻读,但它是MySQL的默认事务隔离级别。
(4)可串行化:读数据需要加共享锁,写数据需要加排它锁,安全性最高,性能最低,但该级别解决了幻读的问题。
悲观锁与乐观锁
在关系型数据库中,按使用方式来说,有两种典型的锁,分别是 悲观锁 与 乐观锁。
悲观锁认为认为数据出现冲突的可能性很大,例子:SELECT ... FOR UPDATE。
乐观锁认为数据出现冲突的可能性不大,例子:对比数据行的时间戳或版本号。
表级锁和行级锁
在关系型数据库中,按粒度来划分的话,有三种类型的锁,分别是 表级锁、行级锁 和 页级锁。我们常用的锁有行级锁和表级锁。
MySQL中表级锁有两类:一是表锁,二是元数据锁。表锁的语法为:lock tables ... read/write。它限制了别的线程,也限定了本线程接下来的操作对象。
表锁一般是在数据库引擎不支持行锁的时候才会被用到,比如MyISAM引擎。
MySQL 5.5引入了MDL(元数据锁),不需要显示使用,在访问一个表的时候会自动被加上。
当对一个表做增删改查时,自动加MDL读锁(读锁之间不互斥);
当对一个表做表结构变更时,自动加MDL写锁(读写锁与写锁之间互斥);
MDL的作用:保证读写的正确性。
MDL带来的问题:修改表结构时申请MDL写锁,可能会导致锁表,后续业务操作均会阻塞。
在MySQL中,行锁是引擎层由各个引擎自己实现的,但并不是所有引擎都支持行锁例如MyISAM就不支持,所以我们默认会使用InnoDB引擎。
所谓行锁,就是针对数据表中行记录的锁。比如,事务A更新了一行,这时事务B也要更新同一行,那么事务B需要等待事务A的操作完成后才能进行。
更多关于表锁和行锁的内容请阅读《》。
脏读
所谓脏读,就是指 读到了未提交事务的数据。
脏读对应的隔离级别是“读未提交”,只有该隔离级别才会出现脏读。
脏读的解决办法是升级事务隔离级别,比如“读已提交”。
不可重复读
所谓不可重复读,就是两次读取的数据中途被修改。
不可重复读对应的隔离级别是“读未提交”或“读已提交”。
不可重复读的解决办法就是升级事务隔离级别,比如“可重复读”。
幻读
所谓幻读,是指在同一个事务内,同一条查询语句在不同的时间段执行会得到不同的结果集。
要解决幻读不能简单升级事务隔离级别到可串行化,因为会让MySQL失去并发处理能力。
行锁解决不了幻读,因为即使锁住所有记录,还是阻止不了插入新数据。
解决幻读的办法是锁住记录之间的“间隙”,为此InnoDB引入了新的间隙锁 (Gap Lock) 和next-key lock锁。
顾名思义,间隙锁,锁的就是两个值之间的空隙。
比如下图所示的某张表t,在该表主键索引(id)上插入了6个记录(0,5,10,15,20,25),因此产生了7个间隙。
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
图片来源:林晓斌《MySQL实战45讲》
间隙锁是专门用于解决幻读这种问题的锁,它锁了行与行之间的间隙,这样就能够阻塞新插入的操作。
因此,划重点:当MySQL InnoDB引擎在一行行扫描的过程中,不仅会给行加上行锁,还会给行的两边的空隙也加上间隙锁。
由于间隙锁(gap lock)仍在存在一些问题,可能会降低并发度和仍然可能导致死锁。因此,MySQL InnoDB为间隙锁引入了一个补充:next-key lock。
那么,问题来了:啥是next-key lock?
所谓next-key lock,它是间隙锁和行锁的合体,每个next-key lock都是前开后闭区间,如 (0,5]。
间隙锁都是开区间,如 (0,5)。
next-key lock帮助MySQL在默认隔离级别下解决了幻读问题,因此它也是MySQL加锁的基本单位。
更多关于间隙锁和next-key lock的介绍请阅读《》
死锁是如何产生的?
死锁一般发生在多线程(两个或两个以上)执行的过程中,因为争夺资源造成线程之间相互等待。
死锁的产生需要具备4个条件:
(1)互斥:多个线程不能同时使用一个资源;
(2)持有并等待:在已经持有了资源1的情形下又申请资源2,但是资源2已经被其他事务占用;
(3)不可剥夺:在事务已经获取到资源1之后,在使用完之前都不可以被其他事务抢占;
(4)循环等待:发生死锁时,必然会存在一个事务,也就是资源的环形链;
如何避免死锁发生?
对于死锁,我们需要有一个基本认知:死锁只有同时满足四个条件时才会发生,并发场景下一旦发生死锁,并没有什么特别好的办法,最好是规避死锁。
因此,要避免死锁,我们需要至少破坏四个条件中的一个(互斥必须满足,可以从其他三个条件出发)。
(1)破解持有并等待:我们可以一次性申请所有的资源;
(2)破解不可剥夺:如果申请不到资源,可以主动释放占有的资源;
(3)破解循环等待:按序申请资源来预防(即资源有序分配原则);
本文总结了MySQL的事务隔离级别和锁的机制相关的知识点,希望能对你有所帮助!不过,本文只是高层概览,更详细的细节还是需要你参考更多扩展资料才能应对面试的需求。下一篇,我们会聚焦MySQL如何优化数据查询的方案,即在读多写少的场景下如何优化查询性能。
参考资料
丁奇,《MySQL实战45讲》(极客时间)
刘海丰,《架构设计面试精讲》(拉勾教育)
施瓦茨,《高性能MySQL(第三版)》(图书)
狼爷,《》(博客园)
👇扫码关注EdisonTalk