面试题(1)MySQL中的锁
首先,MySQL的锁有三种,分别为全局锁,表级锁和行级锁。
全局锁
全局锁是通过flush tables with read lock命令实现,会使整个数据库就处于只读状态,这时其他的线程对数据进行增删改或者对于表结构的修改都会处于阻塞的状态。主要应用中数据库的备份,加上全局锁可以确保备份期间的数据和表结构不被修改,避免备份数据错误。但是加全局锁期间,数据库处于只读状态,此时就不能进行其他的业务,就会造成业务停滞。为了解决这个问题,可以用 InnoDB 的事务快照功能。具体来说,用 mysqldump --single-transaction
启动一个读事务,生成一个“时间点快照”。备份过程中,其他业务可以正常更新数据,但备份读到的还是事务开始时的数据(这就是可重复读隔离级别)。不过有个坑要注意:事务隔离级别只能防数据被改,但防不住表结构被改!比如备份中途执行 ALTER TABLE
,表结构变了,备份出来的表结构可能和数据不匹配,恢复时就会出问题。这时候就需要全局锁了——它能彻底锁死所有修改,包括 DDL。
全局锁的负面影响,主从延迟是什么?
如果在从库上用全局锁备份,从库会暂时停止处理主库同步的 binlog(因为被锁死了)。主库还在正常写入,binlog 积压到从库,等解锁后从库得拼命“赶工”执行积压的日志,导致后面的操作一直延迟。比如用户刚在主库下单,从库可能要等好几分钟才能查到,这就是主从延迟。
根本原因以及解决方案?
从库SQL线程是单线程执行的,如果从库又大事务或者高并发写入,从库可能追不上,DDL操作会锁表,导致从库卡住,后面日志全排队。在MySQL高于5.6的版本中引入并行复制,可以让从库多线程执行binlog,提升回放的速度。
表级锁
MySQL中表级锁分为表锁,元数据锁,意向锁和AUTO-INC锁
表锁
表锁限制本线程和其他进程的读写操作。分为共享锁(读锁)和独占锁(写锁),其中的读锁允许当前的会话或者其他会话读取被锁定的表,但是阻止其他会话以及本会话对这些表进行写操作。写锁允许当前会话对表进行读写操作,但是阻止其他会话对加锁的表进行读和写操作。
锁类型 | 其他会话读操作(表锁的情况下) | 本地会话读操作 | 其他会话写操作 | 本地会话写操作 |
读锁(共享锁) | 允许 | 允许 | 阻塞 | 阻塞 |
写锁(独占锁) | 阻塞(无论MyISAM或InnoDB的表锁) | 允许 | 完全阻塞 | 允许 |
并不是所有的读操作都会自动加锁:只有显式加锁(如 SELECT ... FOR SHARE
或 LOCK TABLES READ
)或某些事务中的操作(如 SELECT ... FOR UPDATE
)才会加锁,普通 SELECT
不加锁。
介绍下LOCK TABLES
当使用 LOCK TABLES
对表加锁时,当前会话在锁未释放前,无论读锁还是写锁,当前会话只能操作被锁定的表,其他表的操作会被拒绝。这是它的设计特性,与存储引擎无关。LOCK TABLES是一个粗粒度的锁机制,这样设计目的是让会话在锁定期间专注特定表的操作,避免因跨表引发的所冲突或复杂性,避免锁顺序混乱导致跨表死锁,可以通过限制会话简化锁的管理。
元数据锁
对数据库进行操作时,会自动给表加锁元数据锁(MDL),对一张表进行CRUD操作,加的读MDL锁,对一张表变更,加的MDL写锁。其中读锁之间兼容,写锁与所有锁互斥。MDL锁为了保证用户对表执行CRUD操作时,防止其他线程对表结构做变更。它是在事务回滚或提交后才释放,说明事务的执行期间,MDL一直持有。
补充问题
如果一个事务正在执行SELECT
(加MDL_READ
),另一个事务尝试执行ALTER TABLE
(需要MDL_EXCLUSIVE
),会发生什么?
MDL_EXCLUSIVE是一个写锁,与其他所有类型不兼容,因此ALTER TABLE会被阻塞,直到当前事务提交。在阻塞期间,其他与读锁相关的新事务都可以被执行,但是会排在ALTER TABLE之后,因为写锁的优先级高,如果是一个长事务,那么会导致ALTER TABLE长时间等待。
如果一个长事务长时间持有MDL锁,会有什么风险?如何避免?
- 阻塞DDL操作:如
ALTER TABLE
无法获取MDL_EXCLUSIVE
,导致表结构变更失败或长时间等待。 - 级聢单点阻塞:若该表是热点表,可能导致大量新事务因等待MDL锁而堆积,引发系统响应延迟。
- 死锁风险:若事务在持有MDL锁期间与其他锁(如行锁)发生死锁,可能被回滚
避免方法
- 缩短事务时长:尽快提交或回滚事务,减少锁持有时间。
- 使用
Online DDL
工具:通过分阶段执行DDL,短暂持有写锁,避免长时间阻塞。 - 参数优化:在DDL中使用
NOWAIT
或WAITN
,控制等待时间,失败后重试
意向锁
意向锁是MySQLInnoDB引擎中的一种表级锁,作用是协调行级锁和表级锁的冲突,提高锁检查效率。在加行锁前,首先在表级加个意向锁,其他事务只要检查表级意向锁,就可以直到是否存在行级锁,这样就可以不遍历全表了。
意向锁的类型分为意向共享锁(S锁)和意向排他锁(X锁),意向锁又InnoDB自动管理,无法手动加/解锁,只能够通过行级锁操作间接触发,意向锁在事务提交或回滚时自动释放。
-
为什么插入意向锁不会阻塞其他插入?”
因为插入意向锁是间隙锁的子类,允许多事务在 同一间隙的不同位置插入(主键不冲突)。 -
“意向锁与 MDL 锁(元数据锁)的关系?”
意向锁是 行级锁的表级信号,而 MDL 锁是 表级元数据操作锁(如 DDL),两者作用域不同,但都涉及表级锁协调。
AUTO-INC 锁
自增锁是为了保证在并发插入时,确保AUTO_INCREMENT列的值不重复,通过不同的锁模式,在性能与连续性之间做权衡。
为什么自增ID可能不连续?
- 事务回滚:分配了ID但插入失败(如事务回滚)。
- 模式2(交叉模式):并发分配时可能跳过中间值(如A拿5,B拿6,但可能直接到8)。
- 批量预分配:模式1下系统会批量分配ID(如一次分配10个),但实际只用3个,剩余7个作废。
三种模式如何选择?
- 选模式0:需要严格连续(如金融系统)。
- 选模式1:默认模式,适合混合场景(单条和批量插入)。
- 选模式2:高并发优先(如电商秒杀),且接受ID不连续,同时确保binlog是ROW格式。
行级锁
InnoDB引擎支持行级锁,MylSAM引擎不支持行级锁
行级锁的类型
- Record Lock:记录锁,仅仅把一条记录锁上。
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
- Next-Key Lock:上面两者组合,包括本身及其范围。
Record Lock
行级锁比表级锁更细,锁粒度更高,分为共享锁(其他事务只能读,不能改)和排它锁(其他事务只能等待)。
为什么 Record Lock 需要基于索引?
InnoDB 的数据存储基于 B+Tree 索引结构,行锁只能通过索引找到具体记录。在无索引时,InnoDB 需要扫描全表,只能锁整个表,导致并发效率低下。
Record Lock 和表锁的关系?
表锁:锁住整张表,任何操作需等待。Record Lock:仅锁一行,其他行可并行操作,并发性更高。
如何避免 Record Lock 引发的死锁?
按相同顺序访问资源(比如总是先查ID=1再查ID=2,避免两个事务相反顺序加锁)。可以通过 SHOW ENGINE INNODB STATUS
定位死锁原因。
非唯一索引有什么陷阱?
锁住所有符合条件的记录,可能触发间隙锁。
Gap Lock
InnoDB 在可重复读(RR)隔离级别下,为索引记录之间的“间隙” 加的锁,防止其他事务在该间隙插入新数据。核心是为了解决幻读问题。
触发条件
- 隔离级别是可重复读。
- 使用条件范围查询
- 未命中精确索引记录,对于非唯一索引,间隙锁生效,对于唯一索引,间隙锁不会生效。
为什么 Gap Lock 只在 RR 级别生效?
- RR 的核心要求:事务期间看到的数据必须一致,不允许幻读。
- Gap Lock 的作用:通过锁定间隙,阻止其他事务插入新数据到当前事务的查询范围内。
- RC 级别:仅通过行锁保证数据一致性,允许幻读(因为只关注已存在的记录)。
Gap Lock 如何影响并发?
- 优点:防止幻读,保证事务一致性。
- 缺点:范围查询可能锁住大量间隙,导致其他事务等待。多个事务锁定交错的间隙范围时可能发生死锁。
如何避免 Gap Lock 的负面影响?
- 优化查询:使用唯一索引的精确查询(避免间隙锁)以及避免大范围的
WHERE
条件,改用更精准的条件。 - 降低隔离级别:若业务允许,可将隔离级别改为 RC(但需权衡一致性需求)。
- 使用覆盖索引:查询时只访问索引列,减少锁的范围。
Gap Lock 和表锁的关系?
- 无索引时:全表扫描会退化为表锁(锁定全表),而非间隙锁。
- 有索引时:仅锁定索引范围内的间隙,并发性更高。