以下内容将从MySQL 锁机制的全局概念出发,逐步讲解表级锁与行级锁的差异,重点剖析 InnoDB 常见的锁类型(记录锁、间隙锁、临键锁、意向锁等),并通过代码示例、ASCII 图解和详细说明,帮助你根据业务场景选用合适的锁类型,避免性能瓶颈与死锁,提升系统吞吐。
目录
1. 为什么要了解锁机制?
在数据库系统中,锁用于控制并发访问,维护数据的一致性与隔离性。随着业务规模增大,并发访问压力越来越高,如果锁机制使用不当,常见的问题包括:
- 性能瓶颈:过度加锁导致并发吞吐下降;
- 死锁:不同事务相互等待,系统回滚部分事务;
- 幻读 / 不可重复读:隔离级别不足时,可能读到不一致数据;
因此,深入理解 MySQL 提供的各类锁,才能根据业务场景选用合适的策略,在 一致性 与 性能 之间找到平衡。
2. 锁分类与基本概念
MySQL 中常见的锁,主要分为表级锁和行级锁,另外 InnoDB 还引入意向锁以配合 MVCC。下面逐一介绍这些概念。
2.1 表级锁(Table-level Locks)
表级锁是 MyISAM 引擎的主要锁机制,也可以在 InnoDB 中使用 LOCK TABLES
手动加表锁。表级锁分为:
共享锁(S Lock)
- 锁定整张表,仅允许读操作,其他事务只能读取,不能写入。
排他锁(X Lock)
- 锁定整张表,禁止任何其他事务的读或写操作。
优缺点
优点
- 实现简单,锁粒度粗,一次锁定全表即可保证一致性,适合小规模或低并发场景;
缺点
- 并发性能差,读写冲突严重时会导致大量等待或阻塞;
示例:表级锁使用
-- 会话 A:
LOCK TABLES mytable WRITE;
-- 此时其他会话无法读写 mytable
-- 执行写操作
UPDATE mytable SET col = 1 WHERE id = 5;
-- 释放锁
UNLOCK TABLES;
-- 会话 B(此时才能访问):
SELECT * FROM mytable;
表级锁是最粗粒度的锁,只要存在写锁就会阻塞所有其他访问,除非你的业务本身并发量极低,一般仅作临时维护或备份时使用。
2.2 行级锁(Row-level Locks)
行级锁由 InnoDB 引擎实现,能够对单条记录或记录间隙进行加锁。行级锁细粒度高,在高并发写场景下更能提升并行度。主要有以下几种:
记录锁(Record Lock)
- 锁定具体的索引记录,仅阻塞对该行的并发写操作;
间隙锁(Gap Lock)
- 锁定索引记录之间的间隙,用于防止插入幻读;
临键锁(Next-Key Lock)
- 组合了记录锁 + 间隙锁,锁定某条记录及其左侧间隙;防止幻读和范围更新冲突;
意向锁(Intention Lock)
- 辅助锁,用于表层面声明事务将要对某些行加何种锁,避免上层锁与下层行锁冲突。
2.3 意向锁(Intention Locks)
当 InnoDB 对某行加**共享锁(S Lock)或排他锁(X Lock)**时,会同时在该表的表级锁结构中设置对应的意向锁:
- 意向共享锁(IS Lock):表示事务将要对某些行加共享锁;
- 意向排他锁(IX Lock):表示事务将要对某些行加排他锁;
作用:如果已存在其他事务对整表加了排他锁(X)或共享锁(S),在加行锁之前就能在意向锁层面 detect 并阻塞,避免盲目尝试加行锁而被阻塞在更深层次。
+-----------------------------------+
| mytable 表 |
| ┌────────────┐ |
| │ 意向锁层 │ ← 在此层检查 |
| └────────────┘ |
| ┌────────────┐ |
| │ 行锁层 │ ← 真正加锁层 |
| └────────────┘ |
+-----------------------------------+
- 当事务 A 在
mytable
某行上加 X 锁时,会先在**意向排他锁层(IX)**标记; - 若事务 B 想对整表加共享锁(S),在意向锁层发现已有 IX,就会阻塞;
意向锁对开发者透明,但了解其作用能帮助你理解为什么某些操作会在表级阻塞。
3. InnoDB 行级锁详解
在 InnoDB 中,真正控制并发的是行级锁。结合 MVCC,多版本读可以避免大多数读锁。下面详细介绍 InnoDB 的行锁类型。
3.1 记录锁(Record Locks)
- 记录锁(Record Lock)即对单条索引记录加锁,保证其他事务无法对该行做写操作。
- 典型场景:
SELECT … FOR UPDATE
、UPDATE
、DELETE
都会对涉及到的记录加 X 锁。
示例:记录锁
-- 会话 A:
START TRANSACTION;
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 在 users 表的 id=5 那一行加了记录排他锁(X Lock)
-- 会话 B(同时执行):
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 5;
-- B 会阻塞,直到 A COMMIT 或 ROLLBACK 释放 id=5 的行锁
- 记录锁仅锁定指定记录,不影响同表其他行并发操作。
3.2 间隙锁(Gap Locks)
- 间隙锁(Gap Lock)用于锁定两个索引记录之间的“间隙”,以防止其他事务在该间隙内插入新记录,从而防止幻读。
- 只在**可重复读(REPEATABLE READ)**与 **可序列化(SERIALIZABLE)**隔离级别下出现,且仅在存在范围扫描(
>、<、BETWEEN
)时触发。
ASCII 图解:间隙锁示意
假设表 t(a INT)
且现有数据:10, 20, 30。B+Tree 叶子按顺序排列为 [10] – gap – [20] – gap – [30] – gap]
。
[10] [20] [30]
│ │ │
gaps: <-∞,10> <10,20> <20,30> <30,∞>
当事务 A 执行
SELECT * FROM t WHERE a BETWEEN 15 AND 25 FOR UPDATE;
- 首先定位到
[20]
记录,并加上记录锁; - 同时在
间隙 (10,20)
与(20,30)
上加间隙锁,阻止其他事务在这两个间隙内插入 15、25、18、22 等值。
- 首先定位到
示例:间隙锁演示
-- 准备数据
CREATE TABLE t (a INT PRIMARY KEY) ENGINE=InnoDB;
INSERT INTO t (a) VALUES (10),(20),(30);
-- 会话 A:
START TRANSACTION;
SELECT * FROM t WHERE a BETWEEN 15 AND 25 FOR UPDATE;
-- 此时对 a=20 加记录锁 (Record Lock),
-- 对 (10,20) 和 (20,30) 加间隙锁 (Gap Lock)
-- 会话 B:
START TRANSACTION;
INSERT INTO t (a) VALUES (18);
-- B 阻塞,因为 18 属于 (10,20) 间隙,A 锁住该间隙
- 如果隔离级别为 READ COMMITTED,则不会加间隙锁,仅加记录锁,因此会允许插入 18。
3.3 临键锁(Next-Key Locks)
- 临键锁(Next-Key Lock)是记录锁 + 间隙锁的组合,锁定某条记录及其左侧的间隙。
- 目的是在 REPEATABLE READ 隔离级别下,既阻止其他事务修改当前记录,也阻止插入到锁定范围内,彻底避免幻读。
ASCII 图解:临键锁示意
对于叶子节点顺序 [10] – gap – [20] – gap – [30]
,如果对 20
加临键锁,则锁定 (10,20]
范围:
10 20 30
│ │ │
/ \ / \ / \
[锁定 (10,20]]
- 任何尝试插入在 (10,20] 范围内的新值(如 15、20)都会被阻塞。
示例:临键锁演示
-- 会话 A:
START TRANSACTION;
SELECT * FROM t WHERE a = 20 FOR UPDATE;
-- 对 a=20 记录加记录锁,同时加 (10,20] 的间隙锁(组合为临键锁)
-- 会话 B:
START TRANSACTION;
INSERT INTO t (a) VALUES (15);
-- B 阻塞,因为 15 在 (10,20] 临键锁范围内
INSERT INTO t (a) VALUES (20);
-- B 也阻塞,因为 20 属于该范围
SELECT ... FOR UPDATE
在 InnoDB 默认隔离级别下会加临键锁,而非仅加记录锁;- 若想只加记录锁(不阻止在该记录左侧插入新值),可执行
SELECT * FROM t WHERE a = 20 LOCK IN SHARE MODE;
或在 READ COMMITTED 隔离级别下,用FOR UPDATE
只加记录锁。
3.4 锁升级与锁合并
- 当某个范围锁定的行数过多,InnoDB 可能会升级为表级锁。不过 InnoDB 通常不会自动将行锁升级成表锁,而是由意向锁与元数据保护机制来控制大范围锁竞争。
- 锁合并(Lock Consolidation):如果一个事务需要锁定同一页上多条记录,InnoDB 可能会将多个锁合并为针对该页的锁,以减少内存和管理开销。
大多数情况下,开发者无需显式关注锁升级,但应了解在极端情况下,过多的行级锁可能影响系统性能。
4. 典型锁场景与代码示例
下面通过常见事务场景,演示锁的类型和效果,并配合 ASCII 图解加深理解。
4.1 使用 SELECT … FOR UPDATE
演示排他锁
场景:保证某行被修改过程中的一致性
CREATE TABLE accounts (
acc_id INT PRIMARY KEY,
balance DECIMAL(10,2)
) ENGINE=InnoDB;
INSERT INTO accounts VALUES
(1, 1000.00),
(2, 500.00);
-- 会话 A:
START TRANSACTION;
SELECT balance FROM accounts WHERE acc_id = 1 FOR UPDATE;
-- 对 acc_id=1 加排他锁 (X Lock)
-- 会话 B:
START TRANSACTION;
SELECT balance FROM accounts WHERE acc_id = 1;
-- 读取旧值 1000.00,可读到快照(MVCC),因为只是读不会阻塞
UPDATE accounts SET balance = balance - 100 WHERE acc_id = 1;
-- B 阻塞,直到 A COMMIT 或 ROLLBACK
-- 会话 A 继续
UPDATE accounts SET balance = balance + 200 WHERE acc_id = 1;
COMMIT;
-- 此时 A 释放锁
-- 会话 B 继续
UPDATE accounts SET balance = balance - 100 WHERE acc_id = 1;
COMMIT;
流程
- A 用
FOR UPDATE
在acc_id=1
上加 X 锁; - B 的普通
SELECT
不加锁,可读取 MVCC 快照中的值; - B 的
UPDATE
需要加 X 锁,发现被 A 占用而阻塞; - A
COMMIT
释放 X 锁后,B 才能加锁并继续。
- A 用
ASCII 图解
时间轴:
A: START ──> SELECT FOR UPDATE (锁 acc_id=1) ──> UPDATE ──> COMMIT (释放锁)
↓
B: START ──> SELECT (快照读) ──> UPDATE (等待锁 acc_id=1) ──> 继续
4.2 幻读场景:间隙锁与临键锁示意
场景:防止幻读的重复读
CREATE TABLE t2 (a INT PRIMARY KEY) ENGINE=InnoDB;
INSERT INTO t2 VALUES (10),(20),(30);
-- 会话 A:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM t2 WHERE a BETWEEN 15 AND 25 FOR UPDATE;
-- 对 a=20 加 X 锁,同时对 (10,20) 和 (20,30) 加 Gap 锁
-- 锁定范围 (10,30),防止幻读
-- 会话 B:
INSERT INTO t2 (a) VALUES (18);
-- B 阻塞,因为 a=18 属于 (10,20) 间隙
SELECT * FROM t2 WHERE a BETWEEN 15 AND 25;
-- B 阻塞,因为需要对 a=20 的记录加 S 锁或读快照?
-- 会话 A 结束后:
COMMIT;
-- 释放所有锁
-- 会话 B 插入成功
说明
- A 使用
FOR UPDATE
执行范围查询,InnoDB 为防止幻读,对范围(10,30)
加锁(临键锁); - B 试图插入新值
18
时,因18
位于已锁定间隙(10,20)
内,被阻塞; - 直到 A 提交释放锁,B 才能插入。
- A 使用
4.3 意向锁示例:并发更新同表不同记录
CREATE TABLE items (
id INT PRIMARY KEY,
qty INT
) ENGINE=InnoDB;
INSERT INTO items VALUES (1, 5),(2, 3),(3, 10);
-- 会话 A:
START TRANSACTION;
SELECT qty FROM items WHERE id = 1 FOR UPDATE;
-- 对 items.id=1 加 X 锁,同时在表的意向层加 IX
-- 会话 B:
START TRANSACTION;
SELECT qty FROM items WHERE id = 2 FOR UPDATE;
-- 对 items.id=2 加 X 锁,同时在表加 IX
-- 与 A 的 IX 不冲突,可并发
-- 会话 C:
START TRANSACTION;
LOCK TABLES items READ;
-- C 试图对整表加 S 锁,但发现已有 IX(A、B),被阻塞
说明
- A、B 分别在不同记录上加 X 锁,同时在表层加 IX;
- C 试图加表级 S 锁,却被意向排他锁(IX)所阻塞。
4.4 死锁示例:交叉更新导致死锁
场景:两个事务交叉更新两行
CREATE TABLE inventory (
product_id INT PRIMARY KEY,
stock INT
) ENGINE=InnoDB;
INSERT INTO inventory VALUES (100, 50), (200, 30);
-- 会话 A:
START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 100 FOR UPDATE;
-- 锁定 (100)
-- 模拟网络/业务延迟
-- SLEEP(5);
UPDATE inventory SET stock = stock - 1 WHERE product_id = 200;
-- 尝试锁定 (200),若 B 已锁定 (200),则等待
-- 会话 B:
START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 200 FOR UPDATE;
-- 锁定 (200)
-- SLEEP(2);
UPDATE inventory SET stock = stock - 2 WHERE product_id = 100;
-- 尝试锁定 (100),此时 (100) 已被 A 锁定
-- 出现循环等待:A 等待 B 释放 (200),B 等待 A 释放 (100)
-- InnoDB 检测到死锁,自动回滚其中一个事务
- ASCII 图解:死锁环路
会话 A 会话 B
┌─────────────┐ ┌─────────────┐
│ 锁定 100 │ │ 锁定 200 │
│ UPDATE ... │ │ UPDATE ... │
│ 等待锁 200 │◄────┐ │ 等待锁 100 │◄───┐
└─────────────┘ │ └─────────────┘ │
└────────────────────────┘
(A 等待 B,B 等待 A,形成死锁)
- InnoDB 会自动回滚等待时间较短或成本较低的事务,避免永久阻塞。
5. 哪种锁更适合你的业务?
根据不同业务场景,应选择合适的锁粒度与类型,以在保证一致性的同时提升并发性能。
5.1 只需粗粒度控制:表级锁适用场景
业务特点
- 对单表并发操作非常低,写操作稀少;
- 维护、报表、数据迁移期间,可短暂加表锁统一操作;
典型场景
- 离线批量导入:对整表做大量写入,期间阻止并发读写;
- 数据迁移 / 备份:导出整个表,此时加读锁保证静态一致性;
示例
-- 数据迁移场景 LOCK TABLES sales READ; -- 读取 sales 表所有数据导出 SELECT * FROM sales; -- 导出完成后 UNLOCK TABLES;
表级锁实现简单,但会阻塞其他并发访问。若业务对并发要求不高,可直接使用,否则应采用行级锁与事务。
5.2 高并发写入:InnoDB 行级锁优势
业务特点
- 需要对同一表进行大量并发写操作;
- 仅少量事务会碰撞在相同记录上,大部分操作可并行;
行级锁优势
- 仅锁定单条记录或范围,其他行可并行读写;
- 结合 MVCC,可让大多数
SELECT
操作成为“快照读”而不加锁;
示例:电商订单表高并发写入
CREATE TABLE orders ( order_id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT, amount DECIMAL(10,2) ) ENGINE=InnoDB; -- 并发场景:N 个线程同时插入订单 INSERT INTO orders (user_id, amount) VALUES (123, 50.00); INSERT INTO orders (user_id, amount) VALUES (456, 100.00); -- 不同线程锁定不同插入位置,仅对新行加插入意向锁,可并发插入
行级锁有效提升并发吞吐,但要注意避免频繁的范围扫描导致间隙锁过多,从而影响插入并发。
5.3 防止幻读:何时使用间隙锁与临键锁
业务特点
- 需要保证在同一个事务中多次读取某个范围结果集的一致性;
- 如银行对账时,需要确保范围查询后,范围内的新插入不会影响事务内后续读取;
使用场景
- 在
REPEATABLE READ
隔离级别下执行范围更新或范围锁定; 例如:
START TRANSACTION; SELECT * FROM inventory WHERE product_id BETWEEN 100 AND 200 FOR UPDATE; -- 对 (100,200) 范围加临键锁,防止其他事务插入新 product_id=150 -- 事务处理… COMMIT;
- 在
注意点
- 如果隔离级别为
READ COMMITTED
,则不会加间隙锁,仅加普通记录锁; - 若业务对幻读不敏感,可将隔离级别调低为
READ COMMITTED
,减少锁竞争;
- 如果隔离级别为
5.4 最小化死锁风险:事务设计要点
统一加锁顺序
- 在多表或多行更新场景中,确保所有事务以相同的顺序访问并加锁;
- 避免 A 先锁行 1 后锁行 2,而 B 先锁行 2 后锁行 1。
缩短事务持锁时间
- 将业务逻辑中耗时操作移出事务,只在真正需要写数据时开启事务;
-- 不佳示例:事务中包含复杂计算 START TRANSACTION; SELECT balance FROM accounts WHERE id=1 FOR UPDATE; -- ↓ 假设此处进行耗时外部 API 调用 UPDATE accounts SET balance = balance - 100 WHERE id=1; COMMIT; -- 优化示例:先计算再进入事务 SELECT balance FROM accounts WHERE id=1; -- 复杂计算与外部调用 START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id=1; COMMIT;
使用短事务与批量提交
- 对于批量更新、删除,分批次提交而非一次性大事务;
-- 分批删除示例 SET @batch_size = 1000; LOOP DELETE FROM logs WHERE created_at < '2023-01-01' LIMIT @batch_size; IF ROW_COUNT() < @batch_size THEN LEAVE; END IF; END LOOP;
设置合理隔离级别
- 如果业务可以容忍幻读,将隔离级别设置为
READ COMMITTED
,避免间隙与临键锁过多;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 如果业务可以容忍幻读,将隔离级别设置为
6. 最佳实践与调优建议
选择合适隔离级别
- 默认
REPEATABLE READ
能避免大多数并发异常,但幻读处理需间隙锁,增加锁竞争; READ COMMITTED
精简为记录锁,可提高并发插入性能,但容忍幻读。
- 默认
合理设计索引与查询
- 避免全表扫描导致大范围锁;将常用查询条件字段建索引,减少 InnoDB 扫描行数;
- 对范围查询加索引,避免过度加锁。
监控锁等待与死锁
- 定期执行
SHOW ENGINE INNODB STATUS
检查死锁日志; - 查询
INFORMATION_SCHEMA.INNODB_LOCK_WAITS
,定位长时间等待的事务;
- 定期执行
分库分表与业务隔离
- 在极端并发写场景,可将热表进行水平分表或分库,将锁竞争分散到多个物理实例;
使用合适的锁语句
- 仅在确实需要强一致性或防止幻读时,使用
SELECT … FOR UPDATE
或LOCK IN SHARE MODE
; - 在只读场景下,使用普通
SELECT
进行快照读,无需加锁。
- 仅在确实需要强一致性或防止幻读时,使用
7. 小结
本文从表级锁与行级锁的基本类型入手,重点讲解了 InnoDB 下的记录锁、间隙锁、临键锁、意向锁,并通过一系列代码示例与ASCII 图解,演示了各类锁的加锁范围与行为差异。根据不同的业务场景,我们总结出以下要点供参考:
- 表级锁 简单易用,但并发性能差,适合临时维护、备份、导出等场景;
- 行级锁(InnoDB) 粒度更细,在高并发写入场景下优势明显,结合 MVCC 可让绝大多数
SELECT
操作无需加锁; - 间隙锁/临键锁 在
REPEATABLE READ
隔离级别下防止幻读,但会影响插入并发;如对幻读不敏感,可换用READ COMMITTED
减少锁竞争; - 意向锁 主要用于表层的锁冲突检测,对用户透明;
- 死锁风险 主要来自交叉更新,需统一加锁顺序、缩短事务时间、分批提交;合理设置隔离级别也是关键;
- 调优建议:定期监控锁等待与死锁、根据业务并发需求选择合适锁策略、结合分库分表将压力分散。
通过理解不同锁在并发与一致性之间的权衡,你可以根据业务需求选择最合适的锁类型与隔离级别,既保证数据一致性,又最大化系统的并发吞吐。