InnoDB一致性读与锁定读全解析
以下内容是 MySQL 官方文档中关于 InnoDB 存储引擎的“一致性非锁定读(Consistent Nonlocking Reads)”和“锁定读(Locking Reads)” 的详细说明,主要涉及事务隔离级别、MVCC(多版本并发控制)、快照读、当前读、锁机制等核心数据库知识。
下面我将用通俗易懂的语言,结合例子,帮你系统地理解这些概念。
🌟 一、核心目标:在高并发下保证数据一致性和性能
InnoDB 需要在多个事务同时操作数据时,做到:
- 数据不冲突(一致性)
- 性能不下降(并发性)
为此,它采用了两种主要机制:
- MVCC(Multi-Version Concurrency Control) → 实现“一致性非锁定读”
- 行级锁 + 锁定读语句 → 实现“精确控制并发修改”
🔹 1. 什么是 Consistent Nonlocking Read(一致性非锁定读)?
✅ 定义:
一致性读 = 你的
SELECT
查询看到的是数据库在过去某个时间点的快照,而不是实时状态。
这就像给数据库拍了一张“照片”,你所有的查询都基于这张照片来看,不管别人怎么改,你看的都是这张照片里的内容。
🧠 关键点:
概念 | 说明 |
---|---|
不加锁 | SELECT 不会阻塞其他事务写入,也不会被别人阻塞。 |
基于快照 | 看到的是事务开始或第一次读时的“历史版本”。 |
多版本控制(MVCC) | 老版本的数据通过 undo log 保存,可以重建出来。 |
📌 隔离级别的影响
(1)默认:REPEATABLE READ
- 整个事务中所有
SELECT
都使用同一个快照。 - 即使别的事务提交了新数据,你也看不到(直到你提交事务)。
-- Session A
SET autocommit=0;
SELECT * FROM t; -- 看到的是“时间点T”的快照-- Session B
INSERT INTO t VALUES (1,2);
COMMIT;-- Session A 再次查询
SELECT * FROM t; -- 仍然看不到 B 插入的行!
-- 因为还是看“时间点T”的快照COMMIT;SELECT * FROM t; -- 提交后重新开始事务,快照更新,现在能看到 (1,2)
⚠️ 这就是文档里说的:你看到的可能是一个从未真实存在过的数据库状态。
比如你更新了一些行,然后SELECT
能看到自己改的,但其他未改的行是旧版本 —— 整体看起来像是“拼接”出来的状态。
(2)READ COMMITTED
- 每次
SELECT
都会创建一个新的快照。 - 所以你能看到其他事务已经提交的最新数据。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 每次 SELECT 都刷新快照
SELECT * FROM t; -- 可能看到刚被别人提交的数据
📌 小结:
REPEATABLE READ
:事务内SELECT
总是看到一样的数据(强一致性)READ COMMITTED
:每次SELECT
都看最新的已提交数据(弱一致性,但更“新鲜”)
❗ 注意:DML 操作(UPDATE/DELETE)不受快照限制!
文档中的重点提示:
一致性读只适用于 SELECT,不适用于 DML!
这意味着:
-- 你在 REPEATABLE READ 下
SELECT * FROM t WHERE c1 = 'xyz'; -- 返回空(你看不到别人刚插入的)DELETE FROM t WHERE c1 = 'xyz'; -- 却可能删掉别人刚插入的记录!
为什么?因为 DELETE
是 DML,它会去读最新的数据版本(当前读),并施加锁。
💡 这就是所谓的“幻读”场景之一 —— 你看不到某行,但它能被你删掉。
🔐 2. 什么时候需要 Locking Read(锁定读)?
当你不只是“看”数据,而是要“基于数据做后续操作”时,快照读就不安全了。
❌ 问题示例:丢失引用完整性
-- 你想插入一个子记录,先查父记录是否存在
SELECT * FROM parent WHERE id = 100; -- 找到了-- 但在这瞬间,另一个事务删除了 parent.id=100
-- 你插入 child 记录就会破坏外键约束!
INSERT INTO child (...) VALUES (...);
👉 快照读无法防止这种情况。
✅ 解决方案:使用 FOR SHARE
或 FOR UPDATE
(1)SELECT ... FOR SHARE
- 给读取的行加共享锁(S锁)
- 别人不能修改或删除这些行,直到你提交
- 适合“检查是否存在”的场景
SELECT * FROM parent WHERE id = 100 FOR SHARE;
-- 如果别人正在删这行,你会等待
-- 保证你看到的 parent 是“受保护”的
(2)SELECT ... FOR UPDATE
- 给读取的行加排他锁(X锁)
- 别人不能读(在某些隔离级别)、不能改、不能删
- 适合“马上要更新”的场景
SELECT counter FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter = counter + 1;
-- 确保没人能同时读取并更新 counter
⚠️ 注意:必须关闭 autocommit 才能生效!
START TRANSACTION;
SELECT ... FOR UPDATE;
-- 其他事务在此期间无法获取该行的锁
COMMIT; -- 锁释放
🛑 3. 特殊选项:NOWAIT
和 SKIP LOCKED
当你不希望等待锁时,可以用:
(1)NOWAIT
- 立即失败,不等待
- 适用于不想阻塞的应用逻辑
SELECT * FROM queue_table FOR UPDATE NOWAIT;
-- 如果行被锁,直接报错:ERROR 3572 (HY000): Do not wait for lock.
(2)SKIP LOCKED
- 跳过被锁的行,只返回未被锁的行
- 常用于“任务队列”消费场景
SELECT * FROM tasks FOR UPDATE SKIP LOCKED LIMIT 1;
-- 多个 worker 并发执行,各自拿不同的任务,互不等待
⚠️ 警告:
SKIP LOCKED
返回的是不一致的数据视图,不能用于金融类事务!
🧩 4. DDL 对一致性读的影响
某些 DDL 会破坏 MVCC 快照机制:
DDL | 是否影响一致性读 | 原因 |
---|---|---|
DROP TABLE | ❌ 不可用 | 表都没了,快照也无法访问 |
ALTER TABLE (重建表) | ❌ 可能出错 | 新表的行不在原快照中,可能报错 ER_TABLE_DEF_CHANGED |
所以建议:不要在长事务中执行 DDL!
🔄 5. 特殊 SELECT:INSERT INTO … SELECT 等
这类语句中的 SELECT
默认行为更强:
INSERT INTO t2 SELECT * FROM t1;
- 即使在
REPEATABLE READ
,这个SELECT
也会像READ COMMITTED
一样,每次读最新快照 - 目的是减少锁冲突,提高性能
如果你想避免锁,可以设置:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 或
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
🎯 总结:一张表帮你理清思路
场景 | 推荐方式 | 原因 |
---|---|---|
只想读数据,不关心是否最新 | SELECT (一致性读) | 高并发,无锁 |
想读最新已提交数据 | SET TRANSACTION READ COMMITTED + SELECT | 每次都刷新快照 |
查数据后要插入/更新相关数据 | SELECT ... FOR SHARE | 防止别人改 |
要更新计数器、分配ID等 | SELECT ... FOR UPDATE | 防止并发竞争 |
消费任务队列 | SELECT ... FOR UPDATE SKIP LOCKED | 高并发取任务 |
不想等锁 | SELECT ... FOR UPDATE NOWAIT | 失败快速返回 |
获取最新数据并加锁 | SELECT ... FOR UPDATE | 当前读,读最新版本 |
💡 一句话总结
SELECT
(无锁):看的是“过去的照片”(MVCC 快照)SELECT ... FOR UPDATE/SHARE
:看的是“现在的实景”,并且“占住不让动”(加锁)NOWAIT/SKIP LOCKED
:不想等人,要么立刻失败,要么跳过
如果你有具体的业务场景(比如“如何安全地生成唯一编号”、“如何避免超卖”),我可以结合这些机制给出完整方案。欢迎继续提问!