并发更新数据库的时候,你有没有遇到过"锁等待超时"或者死锁?
MySQL的锁机制是保证并发安全的核心,但也是出了名的复杂。今天把行锁、表锁、间隙锁这些概念一次讲清楚。
先分清:存储引擎决定锁类型
MySQL的锁机制和存储引擎密切相关:
| 存储引擎 | 支持的锁 | 说明 |
|---|---|---|
| InnoDB | 行锁 + 表锁 | 默认行锁,支持事务 |
| MyISAM | 只有表锁 | 不支持行锁,不支持事务 |
现在基本都用InnoDB了,所以今天主要聊InnoDB的锁。
InnoDB的行锁:基于索引
InnoDB的行锁不是真的"锁行",而是锁索引。如果查询没有走索引,行锁会退化成表锁。
sql
-- 假设id是主键
-- 走索引,只锁id=1这一行
BEGIN;
UPDATE users SET name = '李四' WHERE id = 1;
-- 其他事务可以正常操作其他行
-- 假设name没有索引
-- 不走索引,锁全表!
BEGIN;
UPDATE users SET age = 26 WHERE name = '张三';
-- 其他事务的UPDATE都会被阻塞
教训:UPDATE和DELETE一定要走索引,否则行锁变表锁。
锁的类型
InnoDB有几种关键的锁模式:
共享锁(S锁):读锁。多个事务可以同时持有。
sql
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 其他事务可以加S锁读,但不能加X锁写
排他锁(X锁):写锁。独占,其他事务不能加任何锁。
sql
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 或任何UPDATE/DELETE操作
-- 其他事务不能读也不能写
| 对比 | S锁(共享) | X锁(排他) |
|---|---|---|
| 读-读 | 兼容 | 不兼容 |
| 读-写 | 兼容 | 不兼容 |
| 写-写 | 不兼容 | 不兼容 |
间隙锁(Gap Lock)
间隙锁是InnoDB在REPEATABLE READ隔离级别下的特殊锁------它锁的不是已有的数据行,而是索引之间的间隙。
sql
-- 表中有 id = 1, 5, 10 三条记录
-- 执行范围查询
SELECT * FROM users WHERE id BETWEEN 1 AND 5 FOR UPDATE;
-- 锁住了:
-- id=1 这一行(记录锁)
-- id=5 这一行(记录锁)
-- id在(1,5)之间的间隙(间隙锁)
-- 甚至 id>5 到下一个记录之前的间隙
间隙锁的作用是防止幻读------阻止其他事务在间隙中插入新记录。
sql
-- 事务A
SELECT * FROM users WHERE id BETWEEN 1 AND 5 FOR UPDATE;
-- 事务B(会被阻塞)
INSERT INTO users (id, name) VALUES (3, '王五'); -- 阻塞!因为在间隙中
死锁:两个事务互相等待
sql
-- 事务A -- 事务B
UPDATE users SET age=26 UPDATE users SET age=27
WHERE id=1; WHERE id=2;
-- 事务A锁了id=1,事务B锁了id=2
UPDATE users SET age=26 UPDATE users SET age=27
WHERE id=2; WHERE id=1;
-- 事务A等id=2(被事务B锁住) -- 事务B等id=1(被事务A锁住)
-- 死锁!
MySQL有死锁检测机制,发现后会自动回滚其中一个事务(选择回滚代价小的那个)。
sql
-- 查看死锁信息
SHOW ENGINE INNODB STATUS;
-- 配置死锁检测
SET GLOBAL innodb_deadlock_detect = ON; -- 默认开启
SET GLOBAL innodb_lock_wait_timeout = 10; -- 锁等待超时(秒)
减少锁冲突的实践建议
| 建议 | 说明 |
|---|---|
| 索引必加 | UPDATE/DELETE走索引,避免行锁变表锁 |
| 事务尽量短 | 减少持锁时间 |
| 按固定顺序访问 | 所有事务按相同顺序操作资源,避免死锁 |
| 降低隔离级别 | READ COMMITTED没有间隙锁,并发度更高 |
| 乐观锁替代 | 用版本号代替悲观锁,减少锁冲突 |
乐观锁示例:
sql
-- 不加锁,用版本号控制
UPDATE users SET age = 26, version = version + 1
WHERE id = 1 AND version = 1;
-- 如果version已经不是1了,说明被别人改过了,更新失败
MySQL锁机制是并发编程中容易出问题的地方。理解了行锁的本质(基于索引)、间隙锁的作用(防幻读)、死锁的产生条件,就能排查大部分锁相关的问题。
你们项目里遇到过锁等待或死锁的问题吗?