详细说说 MySQL 数据库中锁的分类#
- 全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
- 表级锁:MySQL 里面表级别的锁有这几种:
- 表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
- 元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
- 意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。
- 行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
- 记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥。
- 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
- Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
- 插入意向锁,当插入位置的下一条记录有间隙锁,那么就会生成插入意向锁,然后进入阻塞状态
回答#
根据锁粒度的不同, MySQL 的锁可以分为全局锁、表级锁、行级锁。
我比较熟悉的是表级锁和行级锁,比如我们对一张表结构进行修改的时候,MySQL 就会对这张表加一个元数据锁,元数据锁是属于表级锁的。
行级锁目前只有 Innodb 存储引擎实现了,MyISAM 存储引擎是不支持行级锁的,只有表锁。Innodb 存储引擎实现的行级锁主要有记录锁、间隙锁、临键锁、插入意向锁这些,当我们对表记录进行 select for update,或者增删改的时候,都会对记录加行级锁。
MySQL 怎么实现乐观锁?#
可以基于版本号来实现乐观锁,修改数据的时候带上版本号(或者时间戳):
UPDATE student SET name = ‘小李’, version= 2 WHERE id= 100 AND version= 1
回答#
可以在数据库表增加一个版本号字段,利用这个版本号字段在数据库中实现乐观锁。
具体的实现,每次更新数据的时候,都要带上版本号,同时将版本+1,比如现在要更新id=1,版本号为2的记录。这时候先要获取id=1的版本号,然后更新语句写成 update table set name = “小明”, version = version+1 where id = 1 and version = 2。
如果这个版本号与表记录中的版本号一致的话,就能更新成功,如果不相等则不进行更新,然后需要重新获取该记录的最新版本号,然后再尝试更新数据。
在线上修改表结构,会发生什么?#
回答#
线上环境可能存在很多事务都在读写这张表,如果对这张表进行了表结构修改,就会发生阻塞,原因是有事务对这张表进行读写操作的时候,会生成元数据读锁,而修改表结构的时候,会生成元数据写锁,这时候就产生了读写冲突,所以修改表结构的操作就会阻塞,并且后续事务的增删查改操作都会阻塞。
创建索引的时候会锁表吗?#
对表结构修改,增加字段,删除字段,增加索引,删除索引,更改索引名字,更改字段名字等等,这些操作都会加MDL写锁(元数据写锁),属于表级锁,会和 MDL 读锁发生读写冲突。
回答#
会的,创建索引的时候会加MDL写锁,如果这时候有其他事务对这张表进行增删查改的话,这些事务就都会被阻塞,原因是有事务对这张表进行读写操作的时候,会生成MDL读锁,这时候就产生了读写冲突。
Innodb 存储引擎中的行级锁有哪些?#
主要是有「记录锁、间隙锁、临键锁、插入意向锁」。
记录锁
Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:
当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
举个例子,当一个事务执行了下面这条语句:
mysql > begin; mysql > select * from t_test where id = 1 for update;就是对
t_test表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。当事务执行 commit 后,事务过程中生成的锁都会被释放。
间隙锁
Gap Lock 称为间隙锁,只存在于可重复读隔离级别和串行化隔离级(读已提交隔离级别不存在间隙锁),目的是为了解决可重复读隔离级别下幻读的现象。
假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。
临键锁
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。
虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。
插入意向锁
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
举个例子,假设事务 A 已经对表加了一个范围 id 为(3,5)间隙锁。

当事务 A 还没提交的时候,事务 B 向该表插入一条 id = 4 的新记录,这时会判断到插入的位置已经被事务 A 加了间隙锁,于是事物 B 会生成一个插入意向锁,然后将锁的状态设置为等待状态(PS:MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),此时事务 B 就会发生阻塞,直到事务 A 提交了事务。
插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。
如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。
插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。
回答#
Innodb 实现的行级锁有记录锁、间隙锁、临键锁、插入意向锁。我们在使用增删改或者锁定读语句的时候,都会对记录加行级锁。
记录锁,可以避免其他事务对该记录进行删除和更新操作
间隙锁,可以避免其他事务往间隙里插入新记录
临键锁,是记录锁和间隙锁的组合,所以它既可以免其他事务对该记录进行删除和更新操作,也可以避免其他事务往间隙里插入新记录
插入意向锁,插入意向锁和间隙锁是互斥的关系,其他事务插入的时候,发现插入位置的下一条记录有间隙锁的话,才会生成的插入意向锁,并且这时候锁的状态是阻塞状态,目的是告诉用户插入的位置存在间隙锁
间隙锁的工作原理是什么?#
间隙锁是可重复读隔离级别和串行化隔离级别下才有的锁(读已提交隔离级别只有记录锁,没有间隙锁),主要是为了防止幻读的问题,它可以阻止其他事务往间隙里插入新记录。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。
回答#
间隙锁防止其他事务往间隙插入新记录,从而可以避免幻读的问题,具体的原理是当其他事务插入记录的时候,当发现插入位置的下一条记录有间隙锁,就会生成插入意向锁,然后锁设置为阻塞状态,目的是告诉用户插入的位置存在间隙锁。
一条 Update 语句没有带 where 条件,加的是什么锁?#
Innodb 加锁是索引加锁,可重复读级别下,加锁的基本单位是next-key锁。读已提交隔离级别下,加锁的基本单位是记录锁。
更新没有带 where 条件,会全表扫描,会对每一条记录都加锁。
回答#
可重复读级别下,更新没有带 where 条件,会全表扫描,会对每一条记录都加next-key锁,相当于锁住了全表。
读已提交隔离级别下, 没有间隙锁,更新没有带 where 条件,是全表扫描,那么会对每一条记录都加记录锁。
带了where条件没有命中索引,加的是什么锁?#
回答#
没有命中索引,是全表扫描,那么:
- 在可重复读级别下,全表扫描的话,会对每一条记录都加 next-key 锁。
- 在读已提交隔离级别下, 因为没有间隙锁,全表扫描的时候,会对每一条记录都加记录锁。
两条更新语句更新同一条记录,加的是什么锁?#
在可重复读级别下,加锁的基本单位是 next-key 锁,但是在一些场景下,会退化成记录锁或者间隙锁。
这个题目更新同一条记录,就认为是等值查询的场景。要考虑这几种情况:
第一种情况:如果更新条件的字段是唯一索引,加什么锁?

第二种情况:如果更新条件的字段是非一索引,加什么锁?

第三种情况:如果更新条件的字段是没有索引,加什么锁?

回答#
在可重复读级别下,可能有这些情况:
- 如果更新条件的字段是唯一索引,还要看更新的记录是否存在:
- 如果存在,那么这条记录加的记录锁,只锁住该条记录;
- 如果这条记录不存在,则加间隙锁。
- 如果更新条件的字段是非唯一索引,还要看更新的记录是否存在:
- 如果存在,由于非唯一索引会存在相同值的记录,所以非唯一索引等值查询,实际上是一个扫描的过程,那么会针对符合更新条件的二级索引记录,加next-key锁,最后扫描到第一个不符合更新条件的二级索引记录就会停止扫描,然后对第一个不符合更新条件的记录加间隙锁,同时,在符合更新条件的记录的主键索引上加记录锁。
- 如果不存在,会对第一个不符合更新条件的二级索引记录加间隙锁。
- 如果更新条件的字段是没有索引或者没有命中索引,那么就是全表扫描,会对每一条记录都加next-key锁。
两条更新语句更新同一条记录的不同字段,加的是什么锁?#
Innodb 加锁是加在行记录索引上的,不是针对更新的字段加锁。所以是不是更新同一个字段,没有关系。只要在这条记录上有更新操作,就会对这条记录加锁。所以这题的加锁的情况,也跟上一题一样。
回答#
在可重复读级别下,可能有这些情况:
- 如果更新条件的字段是唯一索引,还要看更新的记录是否存在:
- 如果存在,那么这条记录加的记录锁,只锁住该条记录;
- 如果这条记录不存在,则加间隙锁。
- 如果更新条件的字段是非唯一索引,还要看更新的记录是否存在:
- 如果存在,由于非唯一索引会存在相同值的记录,所以非唯一索引等值查询,实际上是一个扫描的过程,那么会针对符合更新条件的二级索引记录,加next-key锁,最后扫描到第一个不符合更新条件的二级索引记录就会停止扫描,然后对第一个不符合更新条件的记录加间隙锁,同时,在符合更新条件的记录的主键索引上加记录锁。
- 如果不存在,会对第一个不符合更新条件的二级索引记录加间隙锁。
- 如果更新条件的字段是没有索引或者没有命中索引,那么就是全表扫描,会对每一条记录都加next-key锁。
了解过 MySQL 死锁问题吗?#
回答#
了解过,在并发事务中,当两个事务出现循环资源依赖,这两个事务都在等待别的事务释放资源时,就会导致这两个事务都进入无限等待的状态,这时候就发生了死锁。
MySQL 怎么排查死锁问题?#
获取死锁日志,分析死锁日志
回答#
在遇到线上死锁问题时,我们应该第一时间获取相关的死锁日志。我们可以通过 show engine innodb status 命令来获取死锁信息。
然后就分析死锁日志。死锁日志通常分为两部分,上半部分说明了事务1在等待什么锁,下半部分说明了事务2当前持有的锁和等待的锁。
通过阅读死锁日志,我们可以清楚地知道两个事务形成了怎样的循环等待,然后根据当前各个事务执行的SQL分析出加锁类型以及顺序,逆向推断出如何形成循环等待,这样就能找到死锁产生的原因了。
MySQL 怎么避免死锁?#
要明确 MySQL 死锁是不能完全避免的,得通过一些手段降低死锁的概率。
缩短锁持久的时间,来降低死锁的概率
通过减少间隙锁,来降低死锁的概率
通过减少加锁范围,来降低死锁的概率
通过MySQL参数设置,来降低死锁的概率
设置锁等待超时参数:innodb_lock_wait_timeout
开启主动死锁检测:innodb_deadlock_detect
实际上死锁是不能完全避免的,只要会加锁,在并发的场景就会发生死锁,但是我们可以通过一些手段,降低发生死锁的概率。
MySQL 的锁是在事务提交的时候才会释放的,所以可以通过缩短锁持久的时间,来降低死锁的概率,比如:
- 如果事务中需要锁多个行,要把最可能造成锁冲突的锁的申请时机尽量往后放,这样事务的持久锁的时间就会比较短。
- 避免大事务,尽量将大事务拆成多个小事务来处理,因为大事务占用耗时长,意味着占用锁占用时间长,与其他事务冲突的概率也会变高;
可以通过减少间隙锁,来降低死锁的概率:
- 如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,因为 RC 隔离级别没有间隙锁,可以避免间隙锁导致的死锁;
可以通过减少加锁范围,来降低死锁的概率:
- 给表添加合理的索引,如果不走索引将会为表的每一行记录加行级锁,死锁的概率就会大大增大;
可以通过MySQL参数设置,来降低死锁的概率:
- 设置合适的锁等待超时阈值,当一个事务的等待时间超过该值后,将回滚当前语句 (而不是整个事务),如果要回滚整个事务,请使用“innodb_rollback_on_timeout” 开启值为:ON,开启这个参数之后,锁超时就会对这个事务进行回滚,于是锁就释放了。
- 开启主动死锁检测,主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。

