← 返回 MYSQL 列表

锁机制

锁机制

一、锁分类总览

MySQL 锁
├── 表级锁
│   ├── 表锁(Table Lock)
│   ├── 元数据锁(MDL)
│   └── 意向锁(Intention Lock)
├── 行级锁(Record Lock)
│   ├── 记录锁(Record Lock)
│   ├── 间隙锁(Gap Lock)
│   └── 临键锁(Next-Key Lock)
└── 其他
    ├── 自增锁(AUTO-INC Lock)
    └── 插入意向锁(Insert Intention Lock)

1.1 各存储引擎锁支持

存储引擎 行锁 表锁 页锁
InnoDB
MyISAM
MEMORY

二、表级锁

2.1 表锁(Table Lock)

sql
-- 手动加表锁(不建议在 InnoDB 中使用)
LOCK TABLES users READ;    -- 读锁:其他会话可读不可写
LOCK TABLES users WRITE;   -- 写锁:其他会话不可读写

-- 解锁
UNLOCK TABLES;

特点:开销小、粒度大、并发低。MyISAM 引擎使用表锁,InnoDB 一般不手动加表锁。

2.2 元数据锁(Metadata Lock, MDL)

MySQL 5.5+ 自动管理,保护表结构定义。执行 DML 时加 MDL 读锁,执行 DDL 时加 MDL 写锁。

sql
-- 事务A:查询(持有 MDL 读锁)
BEGIN;
SELECT * FROM users;

-- 事务B:修改表结构(需要 MDL 写锁,被事务A阻塞)
ALTER TABLE users ADD COLUMN age INT;
-- 事务B 被阻塞

-- 如果事务A长时间不提交,事务B和其他所有查询 users 的操作都会被阻塞

MDL 阻塞排查

sql
-- 查看 MDL 等待
SELECT * FROM performance_schema.metadata_locks;

-- 查看阻塞的线程
SHOW PROCESSLIST;
-- 找到 State = 'Waiting for table metadata lock' 的线程

2.3 意向锁(Intention Lock)

意向锁是 InnoDB 自动加的表级锁,目的是快速判断表里是否有行锁,避免逐行检查。

意向锁 说明
IS(意向共享锁) 事务准备给某些行加共享锁
IX(意向排他锁) 事务准备给某些行加排他锁
sql
-- 事务A:给某行加行锁(自动添加 IX 锁)
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 行锁 + IX 锁

-- 事务B:尝试加表锁(MySQL 检查意向锁,发现有 IX 锁,直接等待)
LOCK TABLES users WRITE;  -- 被事务A阻塞

三、行级锁

InnoDB 的行锁是基于索引实现的——通过索引条件加锁,如果 WHERE 条件没有索引,会升级为表锁。

3.1 记录锁(Record Lock)

锁住索引上的一条记录

sql
-- 事务A:锁住 id=1 的记录
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 在 id=1 的主键索引上加锁

-- 事务B:尝试修改其他记录可以
UPDATE users SET name = 'new' WHERE id = 2;  -- 正常执行

-- 事务B:尝试修改 id=1 的记录被阻塞
UPDATE users SET name = 'new' WHERE id = 1;  -- 阻塞等待

3.2 间隙锁(Gap Lock)

锁住两条记录之间的间隙(包括记录之前和之后的范围),防止幻读。

sql
-- 数据:users 表中有 id=1, 3, 5, 7(无 id=2,4,6)
--                     间隙:(1,3)、(3,5)、(5,7)、(7,+∞)

-- 事务A:
BEGIN;
SELECT * FROM users WHERE id > 2 AND id < 6 FOR UPDATE;
-- 锁住:id=3 上的记录锁 + (1,3) (3,5) (5,7) 的间隙锁
-- 实际上加的是临键锁,后面会讲

-- 事务B:尝试插入 id=4(落在 (3,5) 间隙中)
INSERT INTO users (id, name) VALUES (4, 'new');  -- 等待

间隙锁触发的条件

  • 隔离级别为 REPEATABLE READ(默认)
  • 查询使用范围条件(BETWEEN><
  • 等值查询但记录不存在

3.3 临键锁(Next-Key Lock)

记录锁 + 间隙锁的组合,锁住记录以及记录之前的间隙

sql
-- 数据:id=1, 3, 5, 7
-- 临键锁范围:(-∞, 1], (1, 3], (3, 5], (5, 7], (7, +∞)

-- 事务A:
BEGIN;
SELECT * FROM users WHERE id > 3 FOR UPDATE;
-- 锁住:id=5,7 的记录锁 + (3,5], (5,7], (7,+∞) 的临键锁
-- 其他事务不能插入 id=4,5,6,7,8... 也不能修改 id=5,7

临键锁是 InnoDB RR 级别解决幻读的关键机制

3.4 行锁加锁规则总结

sql
-- 等值查询 - 唯一索引 - 命中:
--    只加记录锁
SELECT * FROM users WHERE id = 5 FOR UPDATE;

-- 等值查询 - 唯一索引 - 未命中:
--    加间隙锁
SELECT * FROM users WHERE id = 4 FOR UPDATE;  -- 间隙锁 (3,5)

-- 范围查询:
--    临键锁
SELECT * FROM users WHERE id > 5 FOR UPDATE;

-- 无索引 WHERE 条件:
--    所有记录加行锁 + 间隙锁 → 相当于表锁(性能极差)
SELECT * FROM users WHERE name = '张三' FOR UPDATE;

四、锁冲突与死锁

4.1 死锁示例

sql
-- 事务A:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 锁 id=1
-- 此时事务B已经锁了 id=2
UPDATE account SET balance = balance + 100 WHERE id = 2;  -- 等待事务B释放 id=2
-- 事务B也等待事务A释放 id=1 → 死锁

-- 事务B:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 2;  -- 锁 id=2
UPDATE account SET balance = balance + 100 WHERE id = 1;  -- 等待事务A释放 id=1

4.2 死锁排查

sql
-- 1. 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS \G;
-- 查看 LATEST DETECTED DEADLOCK 部分

-- 2. 查看当前事务
SELECT * FROM information_schema.INNODB_TRX;

-- 3. 查看锁信息
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

-- 4. 查看被阻塞的线程
SHOW PROCESSLIST;

4.3 死锁避免策略

sql
-- 1. 固定操作顺序
-- 坏:不同事务更新顺序不一致
事务A:UPDATE a → UPDATE b
事务B:UPDATE b → UPDATE a

-- 好:所有事务都按相同顺序更新
事务A:UPDATE a → UPDATE b
事务B:UPDATE a → UPDATE b

-- 2. 缩短事务时间
BEGIN;
  -- 快速执行
  UPDATE a SET ...;
  UPDATE b SET ...;
COMMIT;  -- 尽快提交

-- 3. 设置锁等待超时
SET innodb_lock_wait_timeout = 5;  -- 5秒超时

-- 4. 使用 READ COMMITTED 隔离级别(间隙锁少很多)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 5. 控制并发量
-- 使用队列或限流减少同时更新同一行

4.4 锁冲突排查实战

sql
-- 场景:某个 UPDATE 卡住了,排查步骤

-- 步骤1:查看当前所有事务
SELECT trx_id, trx_state, trx_started,
       TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS trx_age_seconds
FROM information_schema.INNODB_TRX
WHERE trx_state = 'RUNNING';

-- 步骤2:查看锁等待
SELECT
    waiting_trx_id,
    waiting_thread,
    blocking_trx_id,
    blocking_thread
FROM performance_schema.data_lock_waits;

-- 步骤3:查看阻塞的 SQL
SELECT thd.PROCESSLIST_ID, thd.PROCESSLIST_INFO
FROM performance_schema.threads thd
JOIN performance_schema.data_lock_waits dlw
  ON thd.PROCESSLIST_ID = dlw.BLOCKING_THREAD_ID;

-- 步骤4:kill 阻塞的线程(紧急情况下)
KILL [CONNECTION] 123;  -- 123 是线程ID

五、并发场景下的锁使用策略

5.1 悲观锁(SELECT ... FOR UPDATE)

sql
-- 场景:库存扣减,使用悲观锁防止超卖
BEGIN;

-- 锁定商品行,防止其他事务读取/修改
SELECT stock FROM inventory WHERE product_id = 100 FOR UPDATE;

-- 检查库存
IF stock >= 1 THEN
    UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
    INSERT INTO orders (product_id) VALUES (100);
    COMMIT;
ELSE
    ROLLBACK;
    RETURN '库存不足';
END IF;

5.2 乐观锁(CAS)

sql
-- 场景:使用版本号或条件实现乐观锁

-- 方式1:版本号
-- 建表时加 version 字段
ALTER TABLE inventory ADD COLUMN version INT DEFAULT 0;

-- 更新时检查版本号
UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE product_id = 100
  AND stock > 0
  AND version = ?;  -- 传入上次查询到的 version

-- 如果 affected_rows = 0,说明数据已被修改,需要重试

-- 方式2:条件校验(无需 version 字段)
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 100
  AND stock > 0;

-- 如果 affected_rows = 0,说明库存不足或条件不满足

5.3 悲观锁 vs 乐观锁

对比 悲观锁 乐观锁
原理 先锁后操作 先操作、冲突重试
并发能力 低(排队) 高(冲突少时)
适用场景 写冲突频繁 读多写少、冲突低
死锁风险
实现复杂度 中(需重试逻辑)
典型实现 FOR UPDATE version 或 CAS

六、实战案例

案例1:扣减库存锁分析

sql
-- 商品表
CREATE TABLE inventory (
    id INT PRIMARY KEY,
    product_id INT,
    stock INT,
    version INT DEFAULT 0,
    INDEX idx_product (product_id)
);

-- 会话1:
BEGIN;
SELECT stock FROM inventory WHERE product_id = 100 FOR UPDATE;
-- 加锁:idx_product 上的临键锁 + 聚簇索引上的记录锁

-- 会话2:此时尝试更新同一商品
UPDATE inventory SET stock = stock - 1
WHERE product_id = 100 AND stock > 0;
-- 被会话1阻塞,等待行锁

-- 会话3:更新其他商品不受影响
UPDATE inventory SET stock = stock - 1
WHERE product_id = 200;
-- 可以正常执行

案例2:间隙锁导致的插入阻塞

sql
-- 数据:orders 表 id=1,5,10

-- 事务A:
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 3 AND 7 FOR UPDATE;
-- 加锁范围:id=5 的记录锁 + (1,5] 和 (5,10] 的间隙锁
-- 实际:临键锁覆盖 (1,5] 和 (5,10]

-- 事务B:以下操作都会被阻塞
INSERT INTO orders (id) VALUES (2);  -- 落在 (1,5) 间隙
INSERT INTO orders (id) VALUES (4);  -- 落在 (1,5) 间隙
INSERT INTO orders (id) VALUES (6);  -- 落在 (5,10) 间隙
UPDATE orders SET ... WHERE id = 5;  -- id=5 有记录锁

-- 事务B:以下操作可以执行
INSERT INTO orders (id) VALUES (11);  -- 落在 (10,+∞) 间隙
UPDATE orders SET ... WHERE id = 1;   -- id=1 不在锁范围
UPDATE orders SET ... WHERE id = 10;  -- id=10 不在锁范围

案例3:死锁模拟与解决

sql
-- 场景:A给B转账 + B给A转账,交叉更新

-- 安全方案:固定更新顺序(按用户ID升序)
BEGIN;

-- 先锁小ID,再锁大ID
IF 1 < 2 THEN  -- id=1 是转出,id=2 是转入
    UPDATE account SET balance = balance - 100 WHERE id = 1;
    UPDATE account SET balance = balance + 100 WHERE id = 2;
ELSE
    UPDATE account SET balance = balance + 100 WHERE id = 2;
    UPDATE account SET balance = balance - 100 WHERE id = 1;
END IF;

COMMIT;

-- 另一种方案:使用锁超时自动回滚
SET innodb_lock_wait_timeout = 3;  -- 3秒超时
SET innodb_deadlock_detect = ON;   -- 默认ON,主动检测死锁

总结

锁类型 粒度 说明
表锁 手动加,InnoDB 中少用
意向锁 自动加,标识行锁存在
记录锁 锁单条索引记录
间隙锁 间隙 锁范围,防幻读
临键锁 行+间隙 RR级别默认
场景 推荐策略
高并发扣库存 乐观锁(version/CAS)
金融转账 悲观锁 + 固定更新顺序
读多写少 乐观锁
写冲突频繁 悲观锁
报表统计 不加锁用 MVCC