MySQL 的锁是 并发控制的核心,用于解决多用户同时操作数据时的冲突(如脏读、不可重复读、幻读、数据不一致)。其核心逻辑是:通过 “锁定” 数据资源,限制不同事务对数据的操作权限,确保事务隔离性(ACID 中的 I)。
锁的设计与存储引擎强相关(InnoDB 是默认且唯一支持事务和行锁的主流引擎),下面结合实际场景,从「分类→核心锁类型→引擎差异→常见问题」逐步拆解:
一、锁的核心分类(4 个维度)
MySQL 锁可从不同维度划分,核心分类如下,先建立整体认知:
| 分类维度 | 具体类型 | 核心作用 |
|---|---|---|
| 锁粒度 | 表锁(Table Lock)、页锁(Page Lock)、行锁(Row Lock) | 控制锁定的范围(粒度越小,并发性能越好,开销越高) |
| 锁模式 | 共享锁(S 锁 / 读锁)、排他锁(X 锁 / 写锁) | 控制操作权限(读 / 写冲突控制) |
| InnoDB 特有 | 意向锁(IS/IX)、记录锁、间隙锁、临键锁(Next-Key Lock) | 适配事务和索引,解决幻读、优化锁检查效率 |
| 并发策略 | 悲观锁(Pessimistic Lock)、乐观锁(Optimistic Lock) | 锁的使用思路(悲观:先锁再操作;乐观:先操作后校验) |
二、基础:锁粒度(表锁 vs 页锁 vs 行锁)
锁粒度决定了 “锁定数据的范围”,是影响并发性能的关键因素。
1. 表锁(Table Lock):最粗粒度的锁
-
定义:锁定整张表,事务操作时,整张表无法被其他事务修改(读锁除外)。
-
支持引擎:所有引擎(MyISAM 默认,InnoDB 也支持,但不常用)。
-
核心特点:
- 优点:开销极小(无需定位行 / 页)、加锁解锁速度快;
- 缺点:并发性能极差(写操作会阻塞所有其他读写,读操作会阻塞写操作)。
-
触发场景:
-
MyISAM 引擎的所有写操作(INSERT/UPDATE/DELETE)默认加表级排他锁,读操作加表级共享锁;
-
InnoDB 执行无索引条件的 UPDATE/DELETE(如
UPDATE user SET age=20,无 WHERE 或 WHERE 条件无索引),会升级为表锁; -
手动加表锁:
LOCK TABLES user READ; -- 加表级共享锁(自己可读,其他事务可读不可写) LOCK TABLES user WRITE; -- 加表级排他锁(自己可读写,其他事务不可读写) UNLOCK TABLES; -- 释放锁(事务结束也会自动释放)
-
2. 行锁(Row Lock):最细粒度的锁
-
定义:仅锁定需要操作的行数据,其他行不受影响(InnoDB 核心锁类型)。
-
支持引擎:仅 InnoDB(依赖聚簇索引,通过索引定位行)。
-
核心特点:
- 优点:并发性能极好(多事务可同时操作不同行);
- 缺点:开销大(需定位行、维护锁结构)、可能引发死锁。
-
触发场景:
- InnoDB 中,带索引条件的 SELECT/INSERT/UPDATE/DELETE 自动加行锁(如
UPDATE user SET age=20 WHERE id=1,id 是主键索引,仅锁 id=1 的行); - 关键:行锁依赖索引!若 WHERE 条件无索引(或索引失效),会升级为表锁(因无法定位具体行)。
- InnoDB 中,带索引条件的 SELECT/INSERT/UPDATE/DELETE 自动加行锁(如
3. 页锁(Page Lock):中间粒度的锁
- 定义:锁定数据页(InnoDB 页大小默认 16KB),介于表锁和行锁之间。
- 支持引擎:BDB 引擎(极少用),InnoDB 仅在特定场景间接使用(如页分裂时的页级锁)。
- 核心特点:开销和并发性能介于两者之间,因实用性低,无需重点关注。
粒度对比总结
| 锁粒度 | 并发性能 | 开销 | 适用场景 | 代表引擎 |
|---|---|---|---|---|
| 表锁 | 极低 | 极小 | 读多写少、单表数据量小(如配置表) | MyISAM |
| 行锁 | 极高 | 极大 | 高并发读写、多表关联(如订单表) | InnoDB |
| 页锁 | 中等 | 中等 | 极少使用 | BDB |
三、核心:锁模式(共享锁 S vs 排他锁 X)
所有锁的本质都是 “控制操作权限”,S 锁和 X 锁是最基础的锁模式,核心规则是 “读写冲突、读读兼容” 。
1. 共享锁(S 锁 / 读锁)
-
作用:允许事务读取数据,但禁止修改数据。
-
加锁方式:
- 自动:InnoDB 中
SELECT ... FOR SHARE(MySQL 8.0+)或SELECT ... LOCK IN SHARE MODE(低版本); - 注意:普通
SELECT语句默认不加锁(InnoDB 快照读机制),仅在显式声明或事务隔离级别为SERIALIZABLE时加 S 锁。
- 自动:InnoDB 中
-
兼容性:与其他 S 锁兼容(多个事务可同时读),与 X 锁互斥(读时禁止写)。
2. 排他锁(X 锁 / 写锁)
-
作用:允许事务修改数据(INSERT/UPDATE/DELETE),禁止其他事务读写。
-
加锁方式:
- 自动:InnoDB 中 INSERT/UPDATE/DELETE 语句默认加 X 锁;
- 显式:
SELECT ... FOR UPDATE(查询时加 X 锁,阻塞其他事务读写)。
-
兼容性:与所有锁(S 锁、X 锁)互斥(写时禁止其他读写)。
锁模式兼容性矩阵(关键!)
| 已持有锁 \ 请求锁 | S 锁(读) | X 锁(写) |
|---|---|---|
| S 锁(读) | 兼容(可共存) | 互斥(阻塞) |
| X 锁(写) | 互斥(阻塞) | 互斥(阻塞) |
实战示例:S 锁与 X 锁的冲突
-- 事务A(读操作,加S锁)
START TRANSACTION;
SELECT * FROM user WHERE id=1 FOR SHARE; -- 加S锁
-- 此时事务B可执行 SELECT * FROM user WHERE id=1 FOR SHARE(兼容)
-- 但事务B执行 UPDATE user SET age=20 WHERE id=1(需X锁)会被阻塞,直到事务A提交/回滚
-- 事务A提交后,事务B的UPDATE才会执行
COMMIT;
-- 事务A(写操作,加X锁)
START TRANSACTION;
UPDATE user SET age=20 WHERE id=1; -- 自动加X锁
-- 此时事务B执行 SELECT * FROM user WHERE id=1 FOR SHARE(需S锁)会被阻塞
-- 事务B执行 UPDATE user SET age=21 WHERE id=1(需X锁)也会被阻塞
COMMIT; -- 事务A提交,锁释放,事务B执行
四、InnoDB 特有锁:解决事务与索引的复杂场景
InnoDB 作为事务安全引擎,在 S/X 锁基础上扩展了 4 类特有锁,核心是「适配聚簇索引」和「解决幻读」。
1. 意向锁(Intention Lock:IS/IX)
-
核心问题:表锁和行锁需要快速判断兼容性(如事务 A 持有行锁,事务 B 申请表锁,如何避免逐行检查行锁?)。
-
定义:意向锁是「表级锁」,用于标识 “表中是否有行锁”,无需手动加锁,InnoDB 自动生成。
-
类型:
- 意向共享锁(IS):事务计划对表中某些行加 S 锁(执行
SELECT ... FOR SHARE前自动加); - 意向排他锁(IX):事务计划对表中某些行加 X 锁(执行 INSERT/UPDATE/DELETE 或
SELECT ... FOR UPDATE前自动加)。
- 意向共享锁(IS):事务计划对表中某些行加 S 锁(执行
-
兼容性:
- IS 锁:仅与表级 S 锁兼容,与表级 X 锁互斥;
- IX 锁:与表级 S/X 锁都互斥(因表中已有行锁,禁止全表操作);
-
作用:减少锁检查开销(判断表锁是否可行时,只需检查意向锁,无需逐行检查行锁)。
2. 记录锁(Record Lock):锁定具体行
-
定义:锁定索引对应的「具体行数据」,仅锁定存在的记录,不锁定间隙(解决 “不可重复读”)。
-
适用场景:InnoDB 隔离级别为
READ COMMITTED(RC)时,或查询条件是「唯一索引等值查询」(如主键 id=1)。 -
示例:
-- id 是主键(唯一索引),隔离级别 RC UPDATE user SET age=20 WHERE id=1; -- 仅锁定 id=1 的行(记录锁) -- 其他事务可操作 id≠1 的行,无冲突
3. 间隙锁(Gap Lock):锁定索引间隙
-
定义:锁定「索引之间的空白区域」(不包含记录本身),用于防止插入新数据(解决 “幻读”)。
-
适用场景:InnoDB 隔离级别为
REPEATABLE READ(RR,默认隔离级别),查询条件是「范围查询」(如id BETWEEN 1 AND 5)。 -
示例:
-- id 是主键,隔离级别 RR SELECT * FROM user WHERE id BETWEEN 1 AND 5 FOR UPDATE; -- 加间隙锁 -- 锁定的范围是 (负无穷,1)、(1,2)、(2,3)、(3,4)、(4,5)、(5, 正无穷) -- 其他事务无法插入 id 在 1~5 之间的新数据(如 id=3),避免幻读 -
注意:间隙锁仅在 RR 级别生效,RC 级别会关闭间隙锁(通过
innodb_locks_unsafe_for_binlog=1控制)。
4. 临键锁(Next-Key Lock):记录锁 + 间隙锁
-
定义:InnoDB RR 级别下的「默认行锁类型」,是 “记录锁 + 间隙锁” 的组合,锁定范围是「左开右闭」。
-
核心规则:锁定索引记录及其左侧的间隙(如索引值为 1、3、5,临键锁范围是 (-∞,1]、(1,3]、(3,5]、(5,+∞))。
-
示例:
-- id 是主键,隔离级别 RR(默认) SELECT * FROM user WHERE id > 3 FOR UPDATE; -- 加临键锁 -- 锁定范围是 (3,5]、(5,+∞)(假设存在 id=5 的记录) -- 其他事务无法插入 id=4、6 的数据(避免幻读),也无法修改 id=5 的行(记录锁) -
作用:RR 级别通过临键锁解决了 “幻读”(事务多次查询同一范围,结果一致),这是 InnoDB 默认隔离级别的核心优势。
五、并发策略:悲观锁 vs 乐观锁
这是锁的 “使用思路”,而非具体锁类型,适用于不同并发场景。
1. 悲观锁(Pessimistic Lock)
-
核心思路:假设并发冲突一定会发生,先加锁再操作(“先锁后做”)。
-
实现方式:InnoDB 的行锁(S/X 锁)、表锁都是悲观锁(自动或手动加锁)。
-
适用场景:写多读少(如订单支付、库存扣减,冲突概率高)。
-
示例(库存扣减,避免超卖) :
-- 事务A:扣减库存(加X锁,阻塞其他事务修改) START TRANSACTION; -- 显式加X锁,防止其他事务同时扣减 SELECT stock FROM product WHERE id=1 FOR UPDATE; UPDATE product SET stock=stock-1 WHERE id=1 AND stock>0; -- 扣减库存 COMMIT; -- 释放锁
2. 乐观锁(Optimistic Lock)
-
核心思路:假设并发冲突不会发生,先操作数据,最后通过 “版本校验” 判断是否冲突(“先做后验”)。
-
实现方式:无需数据库锁,通过业务字段实现(如版本号
version、时间戳update_time)。 -
适用场景:读多写少(如商品详情查询、用户信息查看,冲突概率低)。
-
示例(更新用户信息,避免覆盖) :
-- 1. 表结构添加版本号字段 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, version INT DEFAULT 1 -- 版本号,初始为1 ); -- 2. 事务A:更新用户信息(先查版本,后校验) START TRANSACTION; -- 查询数据时获取当前版本号 SELECT username, version FROM user WHERE id=1; -- 假设版本号=1 -- 更新时校验版本号(仅当版本号匹配时才更新,同时版本号+1) UPDATE user SET username='zhangsan_new', version=version+1 WHERE id=1 AND version=1; -- 判断更新行数,若为0则说明版本已被修改(冲突),回滚 IF ROW_COUNT() = 0 THEN ROLLBACK; END IF; COMMIT; -
优点:无锁开销,并发性能极高;缺点:冲突时需重试,增加业务复杂度。
六、InnoDB 与 MyISAM 锁机制核心差异
| 对比维度 | InnoDB(默认) | MyISAM(已淘汰) |
|---|---|---|
| 锁粒度 | 行锁(为主)+ 表锁 | 仅表锁 |
| 事务支持 | 支持(依赖锁实现隔离性) | 不支持 |
| 锁模式 | S/X 锁、意向锁、临键锁等 | 仅表级 S/X 锁 |
| 并发性能 | 高(多事务可操作不同行) | 低(写阻塞读、读阻塞写) |
| 死锁 | 可能发生(行锁 + 事务) | 不可能(表锁,加锁顺序唯一) |
| 适用场景 | 高并发读写、事务场景(电商、支付) | 只读场景(报表、日志) |
七、锁的常见问题与解决方案
1. 死锁(最常见问题)
-
定义:两个或多个事务互相持有对方需要的锁,导致永久阻塞(如事务 A 持有行 1 的 X 锁,申请行 2 的 X 锁;事务 B 持有行 2 的 X 锁,申请行 1 的 X 锁)。
-
示例:
-- 事务A START TRANSACTION; UPDATE user SET age=20 WHERE id=1; -- 持有id=1的X锁 UPDATE user SET age=30 WHERE id=2; -- 申请id=2的X锁(被事务B阻塞) -- 事务B START TRANSACTION; UPDATE user SET age=25 WHERE id=2; -- 持有id=2的X锁 UPDATE user SET age=35 WHERE id=1; -- 申请id=1的X锁(被事务A阻塞) -- 双方互相阻塞,死锁发生 -
解决方案:
- 统一事务加锁顺序(如所有事务先操作 id=1,再操作 id=2);
- 减少事务长度(避免长事务持有锁过久);
- 避免一次性锁定大量数据(拆分 SQL);
- 排查死锁:
SHOW ENGINE INNODB STATUS;(查看最近死锁日志)。
2. 锁等待超时
-
定义:事务申请锁时,超过指定时间(
innodb_lock_wait_timeout,默认 50 秒)仍未获取到锁,触发超时。 -
解决方案:
- 优化 SQL(确保索引有效,避免行锁升级为表锁);
- 缩短事务执行时间(避免事务中包含非数据库操作,如 RPC 调用);
- 调整超时参数(根据业务设置合理值,如
SET GLOBAL innodb_lock_wait_timeout=10;)。
3. 行锁升级为表锁
-
原因:WHERE 条件无索引(或索引失效),InnoDB 无法定位具体行,只能锁定整张表。
-
示例(错误用法) :
-- username 无索引,导致行锁升级为表锁 UPDATE user SET age=20 WHERE username='zhangsan'; -- 锁定整张表 -
解决方案:
- 为 WHERE 条件字段建立索引(如
CREATE INDEX idx_user_username ON user(username);); - 避免使用无索引的范围查询(如
WHERE age>20若 age 无索引,会锁全表)。
- 为 WHERE 条件字段建立索引(如
八、锁的使用建议(实战总结)
-
优先使用 InnoDB 引擎:仅 InnoDB 支持行锁和事务,是高并发场景的唯一选择;
-
合理设计索引:索引是行锁的基础,避免无索引导致表锁;
-
控制事务粒度:事务尽量 “短平快”,避免持有锁过久;
-
避免锁冲突:
- 读多写少用乐观锁(版本号),写多读少用悲观锁(
SELECT ... FOR UPDATE); - 批量操作拆分小事务(如批量更新 1000 行,拆分为 10 个事务,每个更新 100 行);
- 读多写少用乐观锁(版本号),写多读少用悲观锁(
-
隔离级别选择:
- 默认 RR 级别(临键锁解决幻读,并发性能平衡);
- 读多写少场景可改为 RC 级别(关闭间隙锁,减少死锁概率,但允许幻读);
-
避免显式加表锁:InnoDB 中尽量不用
LOCK TABLES,依赖行锁实现并发控制。
总结
MySQL 锁的核心是「粒度 + 模式 + 引擎」:
- 粒度:行锁(并发优)> 页锁 > 表锁(并发差);
- 模式:S/X 锁控制读写冲突,InnoDB 扩展意向锁、临键锁适配事务;
- 引擎:InnoDB 是唯一支持事务和行锁的主流引擎,MyISAM 已淘汰。
实际开发中,锁问题的根源多是「索引设计不合理」或「事务管理不当」。只要确保索引有效、事务简短、锁粒度足够细,就能最大程度避免锁冲突,提升并发性能。