数据库锁机制

→ 返回 数据库基础

锁是数据库保证并发安全的手段:多个事务同时访问同一资源时,通过加锁协调读写顺序,防止脏写、丢失更新等问题。InnoDB 中 MVCC 负责快照读,锁负责当前读和写,二者配合实现隔离性,见 MVCC、数据库事务。


按粒度分类

锁粒度锁定范围并发度典型场景
全局锁整个数据库最低FLUSH TABLES WITH READ LOCK(全库备份)
表锁整张表低MyISAM DML;InnoDB DDL(ALTER TABLE)
页锁数据页(16KB)中部分引擎支持,InnoDB 不常用
行锁索引记录高InnoDB DML 默认(必须走索引,否则退化为表锁)
间隙锁索引记录之间的间隙—InnoDB RR,防幻读
临键锁(Next-Key Lock)行锁 + 左开右闭间隙—InnoDB RR 默认行锁算法
表锁 ──► 锁住整张 users 表,其他事务无法读写任何行
行锁 ──► 只锁 id=100 这一行,其他行可并发访问
间隙锁 ──► 锁 (10, 20) 区间,阻止 INSERT id=15

按模式分类(InnoDB)

共享锁(S 锁)与排他锁(X 锁)

已有 S 锁已有 X 锁
请求 S 锁✅ 兼容❌ 阻塞
请求 X 锁❌ 阻塞❌ 阻塞
-- S 锁:读锁,允许多事务同时读,阻止写
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;
 
-- X 锁:写锁,独占,阻止其他读写
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
 
-- UPDATE / DELETE 自动加 X 锁
UPDATE orders SET status = 'paid' WHERE id = 1;

意向锁(Intention Lock)

表级锁,不阻塞行锁,只用于表锁与行锁的协调:

意向锁含义
IS(意向共享锁)事务即将在某些行上加 S 锁
IX(意向排他锁)事务即将在某些行上加 X 锁
事务 A 对某行加 X 锁 → 表上自动加 IX
事务 B 请求表级 S 锁 → 与 IX 冲突 → 阻塞

意向锁由 InnoDB 自动管理,开发者无需手动操作。


InnoDB 行锁三种算法

算法锁定范围隔离级别
Record Lock单个索引记录RC / RR
Gap Lock索引记录之间的间隙(不含记录本身)RR
Next-Key LockRecord Lock + 前面的 Gap Lock(左开右闭)RR 默认

间隙锁示例

索引 id 现有值:10、20、30。

-- RR 下
SELECT * FROM orders WHERE id > 15 AND id < 25 FOR UPDATE;
-- 加 Next-Key Lock:(10, 20]、(20, 30)
-- 其他事务无法 INSERT id=18(落在间隙中)

RC 与 RR 的锁差异

隔离级别间隙锁幻读
READ COMMITTED不使用当前读可能幻读
REPEATABLE READ使用当前读被间隙锁抑制
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 间隙锁关闭,并发 INSERT 更高,但当前读可能幻读

快照读 vs 当前读(与 MVCC 配合)

操作类型是否加锁
普通 SELECT快照读否(MVCC ReadView)
SELECT ... FOR UPDATE当前读X 锁
SELECT ... LOCK IN SHARE MODE当前读S 锁
INSERT / UPDATE / DELETE当前读X 锁

库存扣减正确写法:

START TRANSACTION;
 
-- 当前读 + X 锁,锁住该行
SELECT stock FROM products WHERE id = 100 FOR UPDATE;
 
UPDATE products SET stock = stock - 1 WHERE id = 100 AND stock > 0;
 
COMMIT;

锁与索引

InnoDB 行锁加在索引上,不是加在数据行本身:

情况实际加锁
WHERE id = 1(主键)精确行锁
WHERE name = 'Alice'(二级索引)二级索引记录 + 对应主键记录
WHERE age = 25(无索引)全表扫描 → 表锁
-- 无索引:锁全表
UPDATE users SET status = 0 WHERE nickname = 'test';
 
-- 有索引:只锁匹配行
UPDATE users SET status = 0 WHERE id = 100;

设计 WHERE 条件时确保走索引,避免锁升级导致并发骤降。见 索引。


乐观锁 vs 悲观锁

策略思路实现适用
悲观锁假定会冲突,先加锁再操作SELECT ... FOR UPDATE冲突频繁、强一致(库存、余额)
乐观锁假定冲突少,提交时校验版本号 / CAS读多写少、冲突低(文章点赞)

乐观锁(版本号)

-- 表中有 version 字段
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 5;
 
-- affected rows = 0 → 版本已被其他事务修改,业务层重试

死锁

两个或多个事务互相等待对方持有的锁:

T1: 锁 row(id=1) → 等待 row(id=2)
T2: 锁 row(id=2) → 等待 row(id=1)
→ 循环等待 → 死锁

InnoDB 自动检测死锁,回滚代价最小(undo 量最少)的事务:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

预防与处理

手段说明
固定加锁顺序多行更新时按主键升序加锁
缩短事务不在事务内做 HTTP 调用、MQ 发送
小批量更新大批量改数据拆成多批 COMMIT
应用层重试捕获 1213 错误,指数退避重试
降低隔离级别RC 无间隙锁,死锁概率更低(需评估业务)

排查死锁

-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G
-- 关注 LATEST DETECTED DEADLOCK 段
 
-- 8.0+ 性能库
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

元数据锁(MDL)

MySQL 对表结构变更使用的表级锁:

事务 A:SELECT users(持有 MDL 读锁)
事务 B:ALTER TABLE users(需 MDL 写锁)→ 阻塞
事务 C:SELECT users(需 MDL 读锁)→ 也阻塞(B 前面排队)

长事务 + DDL = 全库阻塞。变更表结构前确认无长查询。


全局锁与表锁速查

-- 全局读锁(全库只读,备份用)
FLUSH TABLES WITH READ LOCK;
UNLOCK TABLES;
 
-- 表锁(MyISAM 风格,InnoDB 也支持但不推荐 DML 使用)
LOCK TABLES users READ;
LOCK TABLES users WRITE;
UNLOCK TABLES;

生产环境优先用 mysqldump --single-transaction(InnoDB 一致性快照)代替全局锁。


SERIALIZABLE 隔离级别

所有普通 SELECT 自动加 LOCK IN SHARE MODE,读写完全串行化:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM orders;  -- 自动加 S 锁

性能最差,仅特殊场景使用。


常见坑

问题原因解决
更新慢、并发低WHERE 无索引,锁全表加索引,EXPLAIN 验证
间隙锁导致 INSERT 阻塞RR + 范围当前读改 RC 或缩小锁定范围
死锁频繁多表加锁顺序不一致统一按主键排序加锁
FOR UPDATE 锁不住快照读与当前读混用扣减场景全程当前读
DDL 阻塞全表MDL 与长事务冲突先查 PROCESSLIST,杀长事务

相关