在数据库的世界里,当多个用户同时访问数据库的时候,如果没有合理的控制机制,很容易会出现数据混乱的问题。
比如两个人同时修改同一条数据,最后结果可能就不对了。MySQL 中的锁,就是为了解决这类并发访问问题而设计的。
什么是锁?
假设有一个公共卫生间,只有一个坑位(这就好比一条数据)。
场景1(无锁):A先生进去后不锁门。这时B先生推门而入......场面一度十分尴尬。这就是脏读或数据损坏。
场景2(有锁):A先生进去后反锁了门(这就是加锁)。B先生来了发现门打不开,只好在门口等待(这就是阻塞)。A先生用完出来,B先生才能进去。
所以,锁是数据库为了保证数据在并发访问时的一致性、完整性而设计的一种机制。
MySQL 锁的两大分类维度
MySQL 的锁可以从两个角度看:
1. 按粒度(锁的范围)分:
- 表锁(锁整张表)
- 行锁(只锁一行)
- 页锁(介于两者之间,InnoDB 不用)
2. 按模式(锁的行为)分:
- 共享锁(S):允许多人读
- 排他锁(X):只允许一人操作
- 意向锁(IS/IX):提前"打招呼"
下面我们就从这两个维度出发,一一拆解。
MySQL 常见锁类型详解
1. 行锁(Row Lock)
是什么?
只锁定你操作的那一行数据,别人还能操作其他行。
使用方式(自动 or 手动):
sql
-- 自动加排他锁(X)
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- 手动加排他锁(查询时就锁定)
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 手动加共享锁(S)
SELECT * FROM users WHERE id = 1 FOR SHARE;
使用场景:
- 秒杀扣库存
- 转账(A 减钱、B 加钱)
- 防止重复提交订单
注意:行锁依赖索引 !如果 WHERE 条件没走索引,InnoDB 会退化成锁整张表!
2. 表锁(Table Lock)
是什么?
锁住整张表,别人既不能读也不能写(写锁),或只能读不能写(读锁)。
使用方式(手动):
sql
-- 加读锁
LOCK TABLES users READ;
SELECT * FROM users;
-- 解锁
UNLOCK TABLES;
-- 加写锁
LOCK TABLES users WRITE;
UPDATE users SET name = '张三' WHERE id = 1;
-- 解锁
UNLOCK TABLES;
使用场景:
- MyISAM 引擎(老项目)
- 批量导入数据时临时锁表
- 极少在 InnoDB 中手动使用(会严重降低并发)
3. 共享锁(S 锁)和排他锁(X 锁)
这是两种最基本的锁模式:
| 类型 | 别名 | 能否多人同时持有? | 别人能否读? | 别人能否写? |
|---|---|---|---|---|
| S锁 | 读锁 | 可以 | 可以 | 不行 |
| X锁 | 写锁 | 不行 | 不行 | 不行 |
举个例子:
sql
-- 事务 A
SELECT * FROM products WHERE id = 100 FOR SHARE; -- 加 S 锁
-- 事务 B(可以执行)
SELECT * FROM products WHERE id = 100 FOR SHARE; -- 也加 S 锁,没问题
-- 事务 C(会被阻塞!)
UPDATE products SET stock = stock - 1 WHERE id = 100; -- 需要 X 锁,等待 A 提交
4. 意向锁(IS / IX)
是什么?
意向锁是表级别的锁,用来提前通知:我打算对这张表里的某些行加锁!
- IS(Intention Shared):我准备给某些行加 S 锁。
- IX(Intention Exclusive):我准备给某些行加 X 锁。
特点:
- 完全自动加锁,你无法手动控制。
- 目的是让数据库快速判断:"这张表有没有人在操作行?" 而不用逐行检查。
举例:
当你执行:
sql
SELECT * FROM users WHERE id = 1 FOR UPDATE;
InnoDB 会自动:
- 在表
users上加 IX 锁 - 在
id=1这一行上加 X 锁
此时,如果有人想执行:
sql
LOCK TABLES users WRITE; -- 需要表级 X 锁
数据库一看:"表上有 IX 锁!说明有人在改行",于是直接让它等待。
意向锁就像进教室前喊一声:"我要进去占座位啦!" ------ 让管理员不用一个个开门查。
5. 间隙锁(Gap Lock)
是什么?
不锁具体数据,而是锁住索引之间的空隙,防止别人往中间插入新数据。
什么时候出现?
在REPEATABLE READ(可重复读) 隔离级别下,执行范围查询 + 加锁时自动触发。
例子:
假设 users 表主键有:10, 30
执行:
sql
SELECT * FROM users WHERE id > 15 AND id < 25 FOR UPDATE;
虽然没查到数据,但 InnoDB 会锁住 (10, 30) 这个间隙 ,防止别人插入 id=20。
为什么需要? 如果不锁间隙,另一个事务插入 id=20,你再次查询就会多出一条记录------这就是幻读。
如何关闭?
- 改隔离级别为
READ COMMITTED - 或确保查询条件是唯一索引的等值查询 (如
WHERE id = 20且 id 是主键)
6. 临键锁(Next-Key Lock)------ 默认行为
是什么?
记录锁 + 间隙锁 的组合。
InnoDB 在 RR 隔离级别下,默认使用 Next-Key Lock 来彻底解决幻读问题。
例子:
对 id=20 加锁,实际锁住的是区间 (10, 20](假设前一个主键是 10)。
你不需要写特殊语法,这是 InnoDB 的默认保护机制。
7. 元数据锁(MDL)
是什么?
保护表结构(DDL)不被并发修改。
自动加锁规则:
- 执行
SELECT→ 自动加 MDL 读锁 - 执行
ALTER TABLE→ 需要 MDL 写锁
常见坑:
sql
-- 事务 A(未提交)
START TRANSACTION;
SELECT * FROM orders; -- 加 MDL 读锁
-- 事务 B
ALTER TABLE orders ADD COLUMN status TINYINT; -- 卡住!等 A 提交
解决方案:避免长事务,及时COMMIT!
8. 自增锁(AUTO-INC Lock)
用于控制AUTO_INCREMENT字段的并发插入,确保自增值不重复。
- 插入单行:轻量锁,不影响并发
- 批量插入(如
INSERT INTO ... SELECT):可能短暂锁表
一般无需干预,由参数 innodb_autoinc_lock_mode 控制(默认值 1 已足够)。
死锁
什么是死锁?
- 事务 A 锁了行 1,等着行 2;
- 事务 B 锁了行 2,等着行 1;
- 两人互相等,谁也动不了。
MySQL 怎么处理?
InnoDB 会自动检测死锁,并回滚其中一个事务 (报错:Deadlock found when trying to get lock)。
如何避免?
- 按固定顺序访问数据(如总是先操作 user_id 小的)
- 减少事务持有锁的时间(尽快 COMMIT)
- 避免大事务
- 必要时降级隔离级别(如用
READ COMMITTED减少间隙锁)
下一篇文章我们再做详细介绍。
实用命令:查看当前锁
MySQL 8.0+:
sql
-- 查看当前所有锁
SELECT * FROM performance_schema.data_locks;
-- 查看谁在等锁
SELECT * FROM performance_schema.data_lock_waits;
-- 查看活跃事务
SELECT * FROM information_schema.INNODB_TRX;
总结
markdown
锁的分类
├── 按粒度
│ ├── 行锁(InnoDB 主力)
│ ├── 表锁(MyISAM / 手动)
│ └── 页锁(已淘汰)
│
└── 按模式
├── S(共享锁) → FOR SHARE
├── X(排他锁) → FOR UPDATE / UPDATE
├── IS(意向共享) → 自动
└── IX(意向排他) → 自动
特殊锁类型:
- 间隙锁、临键锁 → 防幻读(RR 级别自动)
- MDL 锁 → 保表结构
- 自增锁 → 保 AUTO_INCREMENT
建议 :
日常开发用 InnoDB 引擎 + 可重复读隔离级别 ,大多数锁问题都能自动处理。
只要注意:别写太长的事务,索引要合理,就能避开大部分锁冲突!
总的来说,MySQL 的锁机制虽然种类不少,但核心目标只有一个:在并发环境下保证数据的一致性和完整性。只要我们合理使用索引、避免长事务、按规范写代码,大多数锁问题都能轻松避开。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《async/await 到底要不要加 try-catch?异步错误处理最佳实践》
《如何查看 SpringBoot 当前线程数?3 种方法亲测有效》