mysql中的锁
这篇笔记梳理 MySQL 在并发读写中的锁机制,先从锁的目的切入,说明它用于保障多用户访问时的数据一致性与完整性,并区分表锁、行锁、页锁等物理层锁,以及乐观锁、悲观锁这类通常由业务或事务语句体现的控制方式。内容重点放在 InnoDB 与 MyISAM 的差异:MyISAM 更常见表级锁,而 InnoDB 默认支持行级锁,UPDATE、DELETE 按索引命中目标行时通常只加排他行锁,INSERT 一般也不会阻塞已有数据。文章提醒,行锁并不等于永远只锁一行,如果 WHERE 条件没有使用索引或无法命中索引,InnoDB 可能出现范围锁、间隙锁,甚至表现得接近表级阻塞,这是排查更新慢和并发冲突时的重要线索。它还解释了 SELECT FOR UPDATE、显式事务与自动提交的关系:即使没有手写 BEGIN,单条 UPDATE 也会在 InnoDB 内部以隐式事务执行,语句完成后自动提交并释放锁,所以很多场景下不容易观察到阻塞。后半部分聚焦 ALTER TABLE、增删字段、同步建索引等 DDL 操作,说明这类操作需要表级排他访问,会等待已有事务释放相关锁,并可能阻塞后续 SELECT、UPDATE、INSERT、DELETE。对于后端开发和运维人员,这篇笔记的价值在于建立一套判断加锁范围、锁释放时机和 DDL 卡住原因的基础框架,并给出生产环境避开高峰、考虑 pt-online-schema-change 或 gh-ost 等在线 DDL 工具的实践提醒。
目录#
[[toc]]
什么是“锁”?#
- *锁(Lock)**是数据库为保证多用户并发访问数据时,数据的一致性与完整性而采取的一种机制。
就像你在一个文档上修改,防止别人同时修改冲突,给它上锁
数据库主要有两类锁:
| 分类 | 子类型 | 简要说明 |
|---|---|---|
| 物理层锁 | 表锁、行锁、页锁 | 作用于数据“位置” |
| 逻辑层锁 | 乐观锁、悲观锁 | 作用于“逻辑业务”,一般由程序实现 |
✅ 1. 表锁(Table Lock)#
- 整张表加锁,其他线程无法读写
- 粒度大,冲突概率高,MyISAM引擎用得多
LOCK TABLES user WRITE;
✅ 2. 行锁(Row Lock)#
- 只锁住当前操作的行
- 粒度小,性能好,InnoDB默认支持
- 举例:
UPDATE user SET name = '张三' WHERE id = 1;
✅ 3. 乐观锁(Optimistic Lock)#
- 假设没有冲突,不加锁
- 提交前检查“版本”或“时间戳”
- 冲突时提示失败
- 一般通过字段控制,如:
UPDATE table SET version = version + 1
WHERE id = 1 AND version = 3;
✅ 4. 悲观锁(Pessimistic Lock)#
- 认为有冲突,加锁保护
- 每次读取/写入都锁住数据(阻塞其他操作)
- InnoDB默认是悲观锁模式,如:
SELECT * FROM user WHERE id = 1 FOR UPDATE;
🔁 四、UPDATE 语句的锁行为#
情况分析(基于 InnoDB 引擎):
- 使用索引可确保行锁,否则可能锁整张表
| SQL语句 | 锁类型 | 说明 |
|---|---|---|
| UPDATE ... WHERE id=1 | 行锁 | 默认加排他锁(X锁),锁住符合条件的行 |
| UPDATE ... 没有索引 | 表锁(或间隙锁) | InnoDB 尝试行锁失败时退化为表锁或意外加范围锁 |
| 事务外操作 | 自动提交 + 加锁 | 没有事务会立即提交释放锁 |
| 加 FOR UPDATE | 显式加锁 | 同样是行锁,但常用于事务读取后锁定数据 |
为什么insert/update 不会锁整张表?#
在使用 InnoDB 引擎 时,MySQL 默认采用的是 行级锁(Row Lock),这是它区别于 MyISAM 表锁的重要优势之一:
| 操作类型 | 加锁级别 | 说明 |
|---|---|---|
| INSERT | 行锁 / 无锁 | 通常不会锁已有数据,只是插入,几乎不会阻塞其他操作 |
| UPDATE ... WHERE id=1 | 行锁 | 会锁住满足条件的行(id=1),不会锁整张表 |
| DELETE ... WHERE id=1 | 行锁 | 同样只锁住目标行 |
⚠️ 但注意:
如果:
- 没有使用索引 或
- WHERE 语句无法命中索引,
那 MySQL 可能会退化为表锁或间隙锁,这是性能隐患!
✅ 二、如果我此时在执行 DDL 操作(比如 ALTER TABLE)会发生什么?
🔒 DDL 操作(如 ALTER TABLE)一定会加表锁!
| 操作 | 锁类型 | 说明 |
|---|---|---|
| ALTER TABLE ... | 表锁 | 会阻塞该表的读写,直到结构变更完成 |
| ADD/DROP COLUMN | 表锁 | 同上 |
| CREATE INDEX(同步) | 表锁 | 会阻塞写入操作(可考虑在线索引) |
如果在执行:
ALTER TABLE user ADD COLUMN nickname VARCHAR(255);
此时其他连接中的以下操作会被 阻塞:
SELECT * FROM user; -- ❌ 阻塞
UPDATE user SET name='a'; -- ❌ 阻塞
INSERT INTO user ... -- ❌ 阻塞
🚫 在该 ALTER TABLE 执行期间:
- 任何对这张表的 读操作(SELECT * FROM user)都会被阻塞
- 任何对这张表的 写操作(UPDATE、INSERT、DELETE)也都会被阻塞
✅ 等ALTER TABLE完成后:
这些被阻塞的操作才会依次执行,或者超时失败(取决于客户端/连接池设置的超时时间)。
🔧 解决建议(生产环境):#
- 避免高峰期执行 DDL 操作
- 使用在线DDL工具(如 Percona 的 pt-online-schema-change、GitHub 的 gh-ost),避免阻塞业务请求
DDL的排队#
Data Definition Language
-
MySQL 的 DDL 操作本质上 需要排他访问表,也就是说:
要等到当前表上没有其他事务正在访问或占锁
🔁 举个例子:
| 时间 | 操作 | 描述 |
|---|---|---|
| T1 | 业务线程A执行 UPDATE user SET ... WHERE id=1 并开启了事务,但没有提交 | 占用行锁 |
| T2 | 你执行了 alembic upgrade,尝试 ALTER TABLE user ADD COLUMN xxx | MySQL 尝试加表锁,发现表上还有事务未提交 → 被阻塞,卡住了 |
| T3 | 业务线程A事务迟迟没提交 | 你这边就一直挂着 |
| 误解 | 实际机制 |
|---|---|
| “DDL 最优先,其他操作应该等它” | ❌ 错!DDL 也要等现有事务释放表相关锁 |
| “表被频繁更新,DDL 就会失败” | ✅ 是的,频繁操作造成的锁占用会阻塞 DDL |
| “表结构改不动是因为 MySQL 不稳定” | ❌ 是因为 DDL 拿不到锁 |
✅ 你的 update 没写事务,为什么还是会“加锁”?
因为InnoDB 默认使用“自动提交模式”
也就是说,如果你没有显式写 BEGIN 或 START TRANSACTION,那么:
UPDATE user SET name = '张三' WHERE id = 1;
这条语句实际上会被 MySQL 当作:
START TRANSACTION;
UPDATE user SET name = '张三' WHERE id = 1;
COMMIT;
所以虽然你“看起来没用事务”,InnoDB 内部还是执行了一个隐式事务。
✅ 这个隐式事务行为的结果是:
UPDATE 时,会加 行锁(InnoDB 默认)
因为你没显式控制事务,所以 语句一执行完就自动提交,锁也就立刻释放了
因此,大多数情况下你看不到锁阻塞的现象
评论
还没有评论,来发第一个吧