MySQL锁机制详解,看这一篇就够了

在数据库的世界里,当多个用户同时访问数据库的时候,如果没有合理的控制机制,很容易会出现数据混乱的问题。

比如两个人同时修改同一条数据,最后结果可能就不对了。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 会自动:

  1. 在表 users 上加 IX 锁
  2. 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)。

如何避免?

  1. 按固定顺序访问数据(如总是先操作 user_id 小的)
  2. 减少事务持有锁的时间(尽快 COMMIT)
  3. 避免大事务
  4. 必要时降级隔离级别(如用 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 种方法亲测有效》

《Java 开发必看:什么时候用 for,什么时候用 Stream?》

《别再乱 new ArrayList!8 大 Java 容器选型案例,一篇看懂》

相关推荐
凯子坚持 c2 小时前
深度解析 MySQL 与 MCP 集成:从环境构建到 AI 驱动的数据交互全流程
人工智能·mysql·交互
xiucai_cs2 小时前
【后端】开发过程中如何尽可能的减少 bug 的产生
后端·bug
祖国的好青年2 小时前
XAMPP出现Error: MySQL shutdown unexpectedly.
数据库·mysql
梓沂2 小时前
dockercompose启动mysql容器和springboot项目容器时,mysql容器启动慢导致springboot项目容器启动失败
数据库·spring boot·mysql
CodeAmaz2 小时前
MySQL 各种锁机制详解
数据库·mysql·mysql锁
愿你天黑有灯下雨有伞2 小时前
Spring Boot 使用FastExcel实现多文件打包 ZIP导出
windows·spring boot·后端
嘟嘟w2 小时前
双亲委派的概念
java·后端·spring
IMPYLH3 小时前
Lua 的 xpcall 函数
开发语言·笔记·后端·游戏引擎·lua
qq_348231853 小时前
MySQL 与 PostgreSQL对比
数据库·mysql·postgresql