MySQL 到底都有哪些锁?本文按三个分类维度给出完整清单,并重点提供三种反查方式——给你一条 SQL、一个隔离级别、一种索引情况,就能查出它会加什么锁。
问「MySQL 都有哪些锁」之所以容易答乱,是因为锁可以从三个完全不同的维度分类。同一个锁会同时属于三个维度各一项(比如一次 UPDATE 加的锁 = 行级 + 排他 + 悲观)。
| 锁 | 怎么用 | 作用与特点 |
|---|---|---|
| FTWRL 全局读锁 |
FLUSH TABLES WITH READ LOCK; |
整个实例进入只读状态:所有写操作(DML、DDL、甚至事务提交)全部阻塞。典型用途是全库逻辑备份。客户端断开连接后自动释放。 |
| readonly | SET GLOBAL readonly=1; |
效果类似但不建议用于备份:拥有 SUPER 权限的用户仍能写入;且如果客户端异常断开,数据库会一直保持只读,风险比 FTWRL 大。 |
| 一致性快照 (推荐替代) |
mysqldump --single-transaction |
在 RR 下启动一个事务拿到一致性视图,备份期间不阻塞写入。前提是引擎支持事务(InnoDB 可以,MyISAM 不行——所以 MyISAM 才不得不依赖 FTWRL)。 |
FLUSH TABLES WITH READ LOCK 会导致整个业务停写。如果备份一定要用,请确保在从库上做,或使用 --single-transaction。这也是为什么「为什么备库备份时主库没事、业务却卡住了」这类故障常常能追溯到一条 FTWRL。
| 锁 | 谁加的 | 什么时候加 | 特点 |
|---|---|---|---|
| 表锁 Table Lock |
用户显式执行 或 MyISAM 自动 |
LOCK TABLES t READ/WRITE |
InnoDB 一般不用(它有行锁)。MyISAM 的默认并发手段。显式加的表锁用 UNLOCK TABLES 释放,且会隐式提交事务。 |
| MDL 元数据锁 |
MySQL 自动加 (MySQL 5.5 引入) |
访问表就加: DML → MDL 读锁 DDL → MDL 写锁 |
不需要显式使用,但最容易踩坑:一个长事务持有 MDL 读锁时,任何 ALTER TABLE 都会被阻塞,后续所有查询又会被这个 DDL 阻塞,形成雪崩。 |
| 意向锁 IS / IX |
InnoDB 自动加 | 加行锁之前 先在表上加意向锁 |
表级锁,但目的只是「打个招呼」:告诉别的事务「这张表里某些行被锁了」。IS/IX 之间互相兼容,只与表级 S/X 锁冲突。 |
| AUTO-INC 自增锁 |
InnoDB 自动加 | 插入带 AUTO_INCREMENT 列的表 | 语句级锁(不是事务级):插入语句结束立即释放,不用等事务提交。MySQL 8.0 默认 innodb_autoinc_lock_mode=2,绝大多数情况下已不再加表级锁。 |
SELECT * FROM information_schema.innodb_trx;。确认没有后再执行 DDL;或者使用 ALTER TABLE ... WAIT n / NOWAIT(MySQL 8.0)设置等待超时,避免无限堆积。
| 锁 | 锁的范围 | 解决什么问题 | 何时出现 |
|---|---|---|---|
| 记录锁 Record Lock |
单条索引记录 | 防止其它事务修改/删除这一行 | 唯一索引等值精确命中时;以及 RC 隔离级别下的绝大多数场景 |
| 间隙锁 Gap Lock |
两条索引记录之间的空隙 (不含记录本身) |
防止在空隙里插入新记录 → 解决幻读 | 只在 RR 下出现;唯一索引等值未命中时也会退化成它 |
| 临键锁 Next-Key Lock |
记录锁 + 该记录前面的间隙 (左开右闭区间) |
既防改又防插,InnoDB 在 RR 下的默认加锁单位 | RR 下的范围查询、普通索引等值查询 |
| 插入意向锁 Insert Intention |
间隙(特殊类型) | INSERT 前声明「我要往这个空隙插」 | 执行 INSERT 时。同一间隙不同位置的插入互不冲突,提升并发 |
加锁方式:SELECT ... LOCK IN SHARE MODE(MySQL 8.0 也可用 FOR SHARE)。
多个事务可以同时持有同一行的 S 锁。持有 S 锁时,别的事务能读,但不能加 X 锁(写被阻塞)。
典型用途:需要读出数据后基于它做更新,且要求这期间数据不被别人改(如库存校验)。
加锁方式:SELECT ... FOR UPDATE、以及 UPDATE / DELETE / INSERT 自动加。
一行上只能有一个 X 锁。持有 X 锁时,别的事务既不能加 S 也不能加 X,读写全被阻塞(普通 SELECT 走 MVCC 快照读除外)。
假设冲突一定会发生,所以每次操作前先加锁,锁住后再改。
-- 典型写法 BEGIN; SELECT stock FROM goods WHERE id=1 FOR UPDATE; -- 加 X 锁 UPDATE goods SET stock=stock-1 WHERE id=1; COMMIT;
适合写多读少、冲突概率高的场景(如扣库存、转账)。代价是加锁开销与阻塞。
假设冲突很少发生,不加锁,提交时用版本号校验是否被别人改过。
-- 表上加 version 列 UPDATE goods SET stock=stock-1, version=version+1 WHERE id=1 AND version=5; -- 判断 affected rows: -- 0 = 被人改过,重试或报错 -- 1 = 成功
适合读多写少、冲突概率低的场景。无锁开销,冲突时需重试。
| 语句 | 加锁情况 | 说明 |
|---|---|---|
SELECT * FROM t WHERE ... |
不加锁 | 快照读(一致性读),走 MVCC,读的是历史版本,所以不会被任何写阻塞。唯一的例外是 SERIALIZABLE 隔离级别。 |
SELECT ... LOCK IN SHARE MODE |
共享锁 S | 当前读。读最新的已提交数据,并加 S 锁(8.0 起也可用 FOR SHARE)。别的事务可读不可写。 |
SELECT ... FOR UPDATE |
排他锁 X | 当前读。加 X 锁,别的事务读写都阻塞。这是悲观锁的标准写法。 |
UPDATE ...DELETE ... |
排他锁 X | 自动加 X 锁(隐式当前读)。先定位记录再上锁,所以定位过程走不走索引,直接决定锁多少行。 |
INSERT ... |
排他锁 X + 插入意向锁 | 插入成功后对新记录加 X 记录锁;插入前在目标间隙加插入意向锁。若遇唯一键冲突,会加 S 锁去检查重复。 |
ALTER TABLE ... |
MDL 写锁 | 表级。与任何 MDL 读锁(来自 DML)互斥,所以长事务会阻塞 DDL。 |
FOR UPDATE、LOCK IN SHARE MODE、UPDATE、DELETE、INSERT):读最新版本并加锁。| 隔离级别 | 间隙锁 / 临键锁 | 并发度 | 说明 |
|---|---|---|---|
| 读未提交 READ UNCOMMITTED | 无 | 最高 | 不加锁也能读到未提交数据(脏读)。生产几乎不用。 |
| 读已提交 READ COMMITTED | 基本没有 | 高 | 只有记录锁,没有间隙锁(外键检查与重复键检查除外)。并发好,但有幻读。很多互联网公司刻意改用 RC 来规避间隙锁带来的死锁。 |
| 可重复读 REPEATABLE READ MySQL 默认 | 有 | 中 | 默认加 Next-Key Lock,既防不可重复读又防幻读。代价是锁范围更大、更易死锁。 |
| 串行化 SERIALIZABLE | 有 | 最低 | 连普通 SELECT 都会加共享 Next-Key Lock,读写全串行。基本不用。 |
所有分析都以 RR 隔离级别 + 当前读(如 UPDATE / FOR UPDATE) 为前提。核心规律:索引越精确,锁得越少;没有索引,锁全表。
| 索引情况 | 查询类型 | 实际加的锁 | 影响范围 |
|---|---|---|---|
| 主键 / 唯一索引 | 等值,命中 | Record Lock | 只锁这一行。最理想。 |
| 主键 / 唯一索引 | 等值,未命中 | Gap Lock | 锁住这个值本该落入的间隙,别的事务不能插进来。 |
| 普通索引 (非唯一) | 等值 | Next-Key Lock | 锁命中的记录 + 两侧间隙,且向右扫描到第一个不满足条件的记录才停。 |
| 任意索引 | 范围查询 ( > < BETWEEN) |
Next-Key Lock | 锁扫描范围内的所有记录及间隙。 |
| 无索引 (全表扫描) | 任意 | 全表 Next-Key Lock | 给扫描过的每一条记录都加锁,效果等同锁表。这是最危险的情况。 |
UPDATE user SET name='x' WHERE phone='138...',如果 phone 字段没建索引,InnoDB 只能全表扫描,于是每一行都被加上 Next-Key Lock。表现就是:一条简单的 UPDATE 把整张表锁死,所有其它写操作全部超时。EXPLAIN 看有没有走索引。
EXPLAIN 说了算)、是否为覆盖索引、RC 下的 semi-consistent read 等因素。上线前请以 EXPLAIN + 实际观测为准。
| 观测对象 | MySQL 8.0 | MySQL 5.7 |
|---|---|---|
| 当前持有的锁 | performance_schema.data_locks | information_schema.INNODB_LOCKS |
| 锁等待关系 | performance_schema.data_lock_waits | information_schema.INNODB_LOCK_WAITS |
| 活跃事务 | information_schema.INNODB_TRX(两版本通用) | |
| 死锁详情 | SHOW ENGINE INNODB STATUS\G 的 LATEST DETECTED DEADLOCK 段 | |
-- ① 看当前所有锁(8.0) SELECT engine, object_name, lock_type, lock_mode, lock_status, lock_data FROM performance_schema.data_locks; -- ② 看谁在等谁(8.0) SELECT * FROM performance_schema.data_lock_waits; -- ③ 看活跃事务(尤其关注运行时间长的) SELECT trx_id, trx_state, trx_started, trx_query FROM information_schema.innodb_trx ORDER BY trx_started; -- ④ 看锁等待与阻塞源头(8.0,一条 SQL 定位问题) SELECT waiting_pid, waiting_query, blocking_pid, blocking_query FROM sys.innodb_lock_waits;
| SHOW ENGINE INNODB STATUS 里的写法 | 实际含义 |
|---|---|
lock_mode X locks rec but not gap | 记录锁(只锁记录,不锁间隙) |
lock_mode X | 临键锁 Next-Key Lock(默认写法) |
lock_mode X locks gap before rec | 间隙锁 |
lock_mode X locks gap before rec insert intention | 插入意向锁 |
lock_mode S | 共享临键锁 |
innodb_lock_wait_timeoutinnodb_deadlock_detect1213(死锁)/ 1205(锁等待超时),做有限次重试 + 退避。这是唯一正确的处理方式。innodb_deadlock_detect=OFF 后仅依赖超时回滚,可显著降低大量线程等同一把锁时的检测开销(但需把 innodb_lock_wait_timeout 调小以快速失败)。-- ① 有没有锁等待?谁等谁? SELECT * FROM performance_schema.data_lock_waits; -- 8.0 SELECT * FROM sys.innodb_lock_waits; -- 更友好(含 SQL 文本) -- ② 有没有长事务 / 未提交事务? SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS secs, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 5 ORDER BY secs DESC; -- ③ 最近一次死锁详情 SHOW ENGINE INNODB STATUS\G -- 看 LATEST DETECTED DEADLOCK 段 -- ④ 杀掉问题连接(谨慎!先确认业务影响) KILL <processlist_id>; -- ⑤ 查看并设置锁等待超时 SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; SET SESSION innodb_lock_wait_timeout = 10; -- ⑥ 查看当前隔离级别 SELECT @@transaction_isolation;
innodb_trx 找长事务 → 再查 data_lock_waits 找阻塞链源头 → 用 EXPLAIN 确认源头 SQL 走没走索引 → 最后才考虑 KILL。先看清楚再动手。
innodb_lock_wait_timeout:默认 50 秒,高并发可调小到 5~10 秒快速失败innodb_deadlock_detect:默认 ON,极高并发可关innodb_autoinc_lock_mode:8.0 默认 2(交错),5.7 是 1FOR UPDATE、LOCK IN SHARE MODE、UPDATE、DELETE、INSERT 才是当前读、才加锁。