一. 面试题及剖析
1. 今日面试题
你熟悉MySQL中的锁吗?
怎么查看当前有没有锁表?
你熟悉MySQL的行级锁和表级锁吗?
MySQL的悲观锁和乐观锁怎么回事?
如何给MySQL设置锁?
2. 题目剖析
你熟悉MySQL中的锁吗?
怎么查看当前有没有锁表?
你熟悉MySQL的行级锁和表级锁吗?
MySQL的悲观锁和乐观锁怎么回事?
如何给MySQL设置锁?
2. 题目剖析
壹哥今天要带大家梳理复习一个很重要的内容,关于MySQL中的锁!这一块的内容与MySQL的性能优化有一定的关系,所以在面试时经常会被问到的,希望各位好好阅读今天的内容。当然关于锁的知识,壹哥在前面曾经详细的分析过线程的锁,如果你对之前的锁掌握的比较深入,那么对今天的内容就不会觉得特别难。前文链接:
高薪程序员&面试题精讲系列67之除了synchronized与Lock,还有哪些锁?
二. MySQL中的锁
1. 锁概念
首先我们还是来了解一下,MySQL中的锁到底是什么。锁是计算机用于协调多个进程或线程并发访问某一资源的一种并发控制机制,而MySQL中的锁是在存储引擎层实现的并发控制机制。即在不同的存储引擎里,MySQL支持的锁机制并不完全相同,不同的存储引擎会以自己的方式实现锁机制。
虽然通过锁机制可以保证数据并发访问的一致性、有效性等数据安全性,但还是像壹哥一直强调的那样,这个世界上就没有十全十美的技术。加锁可能会带来冲突,并且锁的各种操作,包括获得锁、检测锁是否已解除、释放锁等都会消耗一定的资源,这些操作都会影响到数据库的并发访问性能。
2. 锁分类
接下来,本文主要是基于InnoDB存储引擎进行锁分类的讲解。
2.1 按锁的粒度大小划分
明确了锁的概念之后,我们还要知道MySQL中都有哪几种锁,这个答案其实得看怎么分类了,分类不同,得到的答案可能也不太一样。一般情况下,我们常根据加锁的粒度,可以把MySQL里的锁大致分成全局锁、表级锁、页面锁和行锁几类。
- 全局锁:全局锁就是对整个数据库实例进行加锁。
- 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
- 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
- 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
那我们的MySQL到底会采用哪种锁呢?这要分情况讨论:
- InnoDB:MySQL默认的存储引擎是InnoDB,InnoDB默认支持行级锁,但也支持表级锁;
- MyISAM 和 MEMORY:可以支持表级锁;
- BDB:这是可替代InnoDB的事务引擎,支持页面锁,但也支持表级锁。
默认情况下,表锁和行锁都是自动获得的,不需要执行额外的命令。但在有些情况下, 用户需要明确地进行锁表或者进行事务的控制, 以便确保整个事务的完整性,这样就需要使用事务控制和锁定语句来完成。
2.2 按锁的共享性划分
如果我们按照锁的共享与否,可以将锁分为如下几种:
共享锁/读锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁,即其他事务可以读,但不能对该数据集进行写;
排他锁/写锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁,其他事务既不能读也不能写该数据集。
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
意向共享锁(IS):通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加共享锁,那么此时InnoDB会先找到这张表,对该表加意向共享锁之后,再对记录A添加共享锁;
意向排他锁(IX):通知数据库接下来需要施加什么锁并对表加锁。如果需要对记录A加排他锁,那么此时InnoDB会先找到这张表,对该表加意向排他锁之后,再对记录A添加排他锁。
对于以上几种锁,我们需要注意以下几点:
- 意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理;
- 对于UPDATe、DELETE和INSERT语句,InnoDB会自动给涉及到的数据集加排他锁(X);
- 对于普通的SELECT语句,InnoDB不会加任何锁;
- 事务可以通过以下语句显示给记录集加共享锁或排他锁:
共享锁(S):
SELECT * FROM table_name WHERe … LOCK IN SHARE MODE;
排他锁(X):
SELECt * FROM table_name WHERe … FOR UPDATe,执行该语句时,会将对应的索引访问条目加上排他锁(X锁)。
2.3 按锁的悲观、乐观性划分
如果我们按照锁的悲观、乐观性进行划分,可以将锁分为如下几种:
- 乐观锁:顾名思义,就是很乐观,每次操作数据时都假设不会发生并发冲突,所以不会加锁,只是在进行提交操作时检查是否违反了数据完整性,乐观锁会在更新数据时判断在此期间有没有人更新了数据。我们一般可以使用版本号等机制进行实现,像数据库里的write_condition机制,都是基于乐观锁实现的。乐观锁适用于多读的应用类型,这样可以提高吞吐量,但乐观锁不能解决脏读的问题。
- 悲观锁:顾名思义,就是很悲观,每次操作数据时都认为别人会修改,所以每次在操作数据时都会上锁,这样如果有人想操作这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边很多时候就用到了悲观锁,比如行锁,表锁等,读锁,写锁等,这些都是在做操作之前先加锁。
接下来壹哥再把全局锁、行级锁、表级锁、页面锁等分别介绍一下。
3. 全局锁
3.1 简介
全局锁就是对整个数据库实例进行加锁。全局锁的典型使用场景是对全库进行逻辑备份,也就是把整库中的每个表都查询出来存成文本。
3.2 第一种实现方式
MySQL可以通过如下命令对全局加读锁,使得整个数据库处于只读状态:
Flush tables with read lock;
3.3 特点
当全库加读锁之后,其他线程的以下语句将会被阻塞:
- 数据更新语句(数据的增删改);
- 数据定义语句(包括建表、修改表结构等);
- 更新类事务的提交语句。
但如果整个库都处于只读状态时,可能出现以下问题:
- 如果在主库上进行备份,那么在备份期间都不能执行更新,业务基本上就会停摆;
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的binlog,会导致主从延迟;
- 如果在可重复读的隔离级别下开启一个事务,可以拿到一致性视图。
3.4 第二种实现方式
另外其实还有一个命令,也可以使得全库处于只读状态,该命令如下:
set global readonly=true;
但readonly的值可能会被用来执行其他功能,比如用来判断一个库是主库还是备库,因此修改global变量的值影响可能会很大。
3.5 两种方式的区别
壹哥在上面给大家介绍了两种实现全库加锁的方式,但两种实现方式具有如下一些差异。
- 影响程度不同:readonly的影响程度更大,我们应该追求影响更小的操作实现方案;
- 异常处理机制不同:在执行Flush tables with read lock命令后,如果客户端由于异常而断开,MySQL会自动释放全局锁,整个数据库会回到正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,数据库会一直保持readonly状态,这会导致整个库长时间处于不可写的状态,风险较高。
4. 行级锁
4.1 简介
行锁是针对数据表中行记录的锁,比如某个事务A更新了一行,而这时事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新。行级锁分为共享锁(读锁)和排他锁(写锁),是在存储引擎层进行实现的,但并不是所有的存储引擎都支持行锁,比如MyISAM引擎就不支持行锁。
在行级锁中,会最大程度的支持并发,但与此同时锁的开销也是最大的。在InnoDB的事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才会释放,这个就是两阶段锁协议。如果事务中需要锁多个行,则要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
在 InnoDB中,除单个 SQL 组成的事务外,行级锁都是逐步获得的,因此行级锁有可能会发生死锁。并且由于MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,当多个session访问不同行记录时,但如果使用了相同的索引键, 就会出现锁冲突。所以行级锁具有开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高等特点。
4.2 使用场景
根据以上特点,行级锁适用于有大量的按索引条件进行并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
4.3 隐式加锁
默认情况下,InnoDB的行锁是通过给索引项加锁来实现的,所以只有通过索引项来操作数据才会有行锁,如果没有操作索引项使用用的就是表锁。默认情况下,InnoDB是隐式加锁,即不需要我们进行特别的操作,InnoDB就会自动给我们操作的数据加上行锁或表锁!其中行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件来检索数据时才会用到行锁,否则InnoDB将会使用表锁。这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。例如:
select * from table_name where id = 1 for update
id 字段是唯一索引字段,所以使用的就是行锁,且是排它锁。
我们要记住,只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!所以不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。即使是在MySQL的执行计划中,也只有执行计划真正使用了索引,才能使用行锁。
另外对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及到的数据集加写锁–排他锁(X);而对于普通的SELECT语句,InnoDB则不会加任何锁,因为不需要操作数据,只是读取数据。
4.4 显式加锁
除了隐式加锁,我们也可以自己手动实现,这就是所谓的显式加锁,显式加锁的操作格式如下。
共享锁(S):
SELECT * FROM table_name WHERe ... LOCK IN SHARE MODE
排他锁(X):
SELECt * FROM table_name WHERe ... FOR UPDATe
5. 表级锁
5.1 简介
MySQL的表级锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。其实在上面的章节中,壹哥其实就简单说过表级锁的一些特点,比如开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低等。根据以上特点,我们可以总结出表级锁的使用场景,主要是适合于以查询为主,并发用户少,只有少量按索引条件更新数据的Web应用中。
5.2 实现方式一
在这种实现方式中,存储引擎总是会一次性同时获取所有需要的锁,并且会按相同的顺序获取表锁来避免死锁,具体获取表级锁的用法可以参考如下语句:
select * from table_nane where name = ‘一一哥’ for update
这里的name字段不是唯一索引字段,所以是表锁。
5.3 实现方式二
我们加表锁的另一种实现方式是可以采用如下语句:
#基本语法 lock tables … read/write; #具体语句 LOCK TABLES user READ;
然后我们可以使用SHOW OPEN TABLES WHERe in_use>0;命令,来查看当前处于锁定状态的表有哪些,结果如下:
我们要注意,lock tables命令除了会限制别的线程进行读写之外,也限定了本线程接下来的一些操作。比如某个线程A执行lock tables t1 read,t2 wirte;语句,则其他的线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作,连写t1也都不允许。
5.4 释放锁
除了可以加锁,我们还可以使用unlock tables命令来主动释放锁。另外客户端在断开服务器连接时其实会自动释放锁,不需要我们进行显式的释放操作。
5.5 MDL元数据锁
表锁的第二种形式就是MDL元数据锁,这是在MySQL5.5版本中引入的表锁,主要是用于保证读写的正确性。当我们对一个表进行增删改查操作时,可以加MDL读锁;当要对表做结构变更操作的时候,可以加MDL写锁。但MDL不需要我们显式地使用,在访问一个表时会被自动加上。
MDL锁具有如下特点:
- 读锁之间不互斥,因此可以有多个线程同时对一张表增删改查;
- 读写锁之间、写锁之间互斥,用来保证变更表结构操作的安全性。因此,如果有两个线程同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
6. 页面锁
所谓表锁锁表,行锁锁行,而页锁则会折中,锁定的是相邻的一组数据。页面锁的开销和加锁时间界于表锁和行锁之间,也会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般。
7. 查看是否锁表
壹哥在上面给大家讲解了各种锁及其特点,那我们的数据库现在到底有没有加上锁了呢?该怎么查看才能知道呢?我们可以通过如下语句查看是否锁表:
show open tables where in_use>0;
执行结果如下图所示:
因为壹哥现在的数据库中没有锁表,所以下面的结果中没有具体的数据值,如果你的数据库锁表了,就会有具体的值。
8. 查看正在加锁的事务
8.1 查看正在加锁的事务
我们除了可以查看表是否加锁,还可以查看事务是否被加锁了,语句如下:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
执行结果如下图所示:
8.2 查看等待锁的事务
另外我们还可以通过如下语句来查看正在等待锁的事务:
SELECt * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
执行结果如下图所示:
9. 小结
根据以上所述,我们知道,数据库的并发问题主要是通过事务的隔离级别来解决,而事务隔离级别一般则通过锁机制来实现。我们需要记住MySQL默认的几个关键点:
- MySQL默认存储引擎是InnoDB,默认的隔离级别是可重复读(RR),RR是使用 MVCC+锁混合 的模式来解决脏读、不可重读、幻读等问题;
- 在Innodb引擎下,默认的锁级别是行锁;
- where筛选条件中使用索引字段的,加的是行锁;不是使用索引字段筛选的,加的是表锁;
- 意向共享锁和意向排它锁是数据库主动加的,不需要我们手动处理;
- 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及到的数据集加排他锁;
- 对于普通SELECT语句,InnoDB不会自动加任何锁,但我们可以自己手动上锁。
三. 死锁(Deadlock Free)
1. 死锁产生
壹哥在之前给大家讲线程的死锁时,就给大家讲过死锁产生的原因。死锁是指在并发系统中,不同线程出现了循环资源依赖,涉及到的所有线程都在等待别的线程释放资源,就会导致这几个线程都进入无限等待的状态,称为死锁。而在数据库里也有死锁,这个死锁是指两个或多个事务在同一资源上进行占用,并请求锁定对方占用的资源,从而导致恶性循环。
我们要注意,在MySQL中,即使是以同样的顺序执行SQL语句,有些存储引擎会产生死锁,有些就不会,这是因为锁的行为和执行顺序与存储引擎的底层实现相关。所以死锁的产生与两方面因素有关,一是并发访问数据产生了冲突,二是存储引擎的实现方式。
2. 死锁解决
那如果真的产生了死锁,我们该怎么办?就这样等死吗?那不行,我们肯定想办法打破死锁,解决存在的问题!
首先如果出现了死锁,我们可以使用如下命令来确定最后一个死锁的产生原因:
#老版本的命令 #SHOW INNODB STATUS #新版本的命令 show engine innodb status;
在上述命令的执行结果中,会包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,被回滚的事务等,据此我们就可以分析死锁产生的原因和改进措施。
这样明确了死锁原因之后,我们就可以想办法来解决死锁了。
当死锁发生以后,我们只要部分或完全回滚其中的一个事务,就能打破死锁!InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。所以事务型的应用程序在设计时就要考虑如何处理死锁,我们在多数情况下只需要重新执行因死锁回滚的事务即可,比如添加相关的事务回滚处理代码即可。
3. 锁等待超时机制
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回滚,从而让另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这就需要通过设置锁的等待超时参数 innodb_lock_wait_timeout 来解决。设置了该参数之后,即使是因为外部锁或表锁导致了死锁,我们无法去手动控制这种锁的释放,但因为有超时机制,超时之后就会自动释放锁。
4. 死锁的影响
如果我们的数据库中产生了死锁,到底会怎么样呢?其实死锁作为一种异常现象,带来的影响肯定不会是好的结果,肯定是会降低我们程序的性能。但大家也不用太过于担心,其实数据库的死锁并不会造成很严重的错误,比如InnoDB就会自动检测死锁状况并回滚其中一个受影响的事务。
而在高并发系统上,当多个线程等待同一个锁时,死锁检测机制可能导致执行速度变慢。 当发生死锁时,其实有时候禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以使用innodb_lock_wait_timeout设置进行事务回滚。
5. InnoDB避免死锁的方法
对于我们开发来说,死锁产生后,肯定要想办法解决,这一点壹哥在上面的章节中已经讲过了。但更重要的其实是要避免死锁的产生,所谓的”上医治未病“既是如此!代码中没有异常,总比产生了异常造成了后果再去解决要好,所以接下来壹哥再说一些InnoDB避免死锁产生的方式。
- 对单个InnoDB表上执行多个并发写操作时,可以在事务开始时,通过为想要修改的每行数据使用SELECT ... FOR UPDATE语句,来获取必要的锁;
- 在事务中,如果我们要更新数据记录,可以直接申请足够级别的锁,即排他锁。不要先申请共享锁、等到更新时再申请排他锁,避免用户申请排他锁时,其他事务可能又获得了相同记录的共享锁,从而造成锁冲突,甚至死锁;
- 如果一个事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。如果有不同的程序并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会;
- 通过SELECT ... LOCK IN SHARE MODE获取行的读锁后,应当避免当前事务对该记录进行更新操作,否则有可能会造成死锁;
- 选择合适的事务隔离级别,我们要根据业务需求,设置合适的事务隔离级别。
6. 锁的性能优化
我们使用数据库时,经常需要进行加锁操作,所以除了要避免死锁的产生,还要想办法来提升锁的性能,这样项目的性能才能更高。以下是壹哥给大家总结的一些优化锁性能的方法:
- 尽量使用较低的隔离级别;
- 精心设计索引, 并尽量使用索引访问数据, 使加锁更精确, 从而减少锁冲突的机会;
- 选择合适的事务范围,小事务发生锁冲突的几率更小;
- 给记录集显式加锁时,最好一次性就请求足够级别的锁;
- 不同的程序访问同一个表时,应尽量约定以相同的顺序访问表,对同一个表而言,尽可能以固定的顺序存取表中的行;
- 尽量用相同的条件访问数据,这样可以避免间隙锁对并发插入的影响;
- 不要申请超过实际需要的锁级别;
- 非必要查询时不要显式加锁,MySQL的MVCC机制可以实现事务中的查询不加锁,优化事务性能;
- 对一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。
这样我们通过上面的设计和优化措施,就可以减少死锁的产生,但这并不能完全避免死锁。所以为了让我们的项目更稳健,我们一定要在项目中添加捕获和处理死锁的异常代码。
四. 结语
至此,壹哥就把数据库中与锁相关的内容梳理了一下,现在你对MySQL中的锁熟悉了吗?如果你对锁还有不明白的地方,可以在评论区给壹哥留言,我们一起讨论学习。原创不易,你的点赞是壹哥继续创作下去的动力。



