一文讲透 MVCC:普通 SELECT 何时不加锁?(RC/RR 实战篇)

一文讲透 MVCC:普通 SELECT 何时不加锁?(RC/RR 实战篇)

  • 一、结论先行
  • [二、MVCC 是怎么做到"不加锁也能读"的?](#二、MVCC 是怎么做到“不加锁也能读”的?)
  • 三、什么时候真的"不加锁"?(对照表)
    • [✅ 不加锁的情况(99% 的查询)](#✅ 不加锁的情况(99% 的查询))
    • [❌ 一定会加锁的情况(当前读)](#❌ 一定会加锁的情况(当前读))
  • [四、最关键的概念:快照读 vs 当前读](#四、最关键的概念:快照读 vs 当前读)
    • [1️⃣ 快照读(Snapshot Read)](#1️⃣ 快照读(Snapshot Read))
    • [2️⃣ 当前读(Current Read)](#2️⃣ 当前读(Current Read))
    • 五、用一个时间线彻底看懂
      • 初始数据
      • [事务 A(先开始)](#事务 A(先开始))
      • [事务 B(后开始)](#事务 B(后开始))
      • [事务 A 再查一次](#事务 A 再查一次)
        • [在 RR 下:](#在 RR 下:)
        • [在 RC 下:](#在 RC 下:)
  • 六、那"什么时候会偷偷加锁"?(非常容易踩坑)
      • [⚠️ 场景 1:`SELECT ... FOR UPDATE`](#⚠️ 场景 1:SELECT ... FOR UPDATE)
      • [⚠️ 场景 2:唯一索引 vs 非唯一索引](#⚠️ 场景 2:唯一索引 vs 非唯一索引)
      • [⚠️ 场景 3:SERIALIZABLE 隔离级别](#⚠️ 场景 3:SERIALIZABLE 隔离级别)
  • [七、意向锁 & MVCC 的关系](#七、意向锁 & MVCC 的关系)
  • 八、总结

一、结论先行

InnoDB 在"普通 SELECT + RC / RR 隔离级别"下,使用 MVCC 读历史版本,不加任何行锁,也不加意向锁。

也就是说:

sql 复制代码
SELECT * FROM table WHERE ...

在绝大多数情况下:

❌ 不加行锁

❌ 不加意向锁

❌ 不阻塞写

❌ 不被写阻塞


二、MVCC 是怎么做到"不加锁也能读"的?

3个核心组件

1️⃣ Undo Log :保存行的历史版本

2️⃣ Read View :当前事务"能看到哪些事务"

3️⃣ 隐藏字段

  • trx_id(最后一次修改该行的事务)
  • roll_pointer(指向 undo log)

👉 普通 SELECT:

  • 直接走 undo log
  • 构造一个 一致性视图
  • 完全不碰锁

三、什么时候真的"不加锁"?(对照表)

✅ 不加锁的情况(99% 的查询)

SQL 是否加锁 原因
SELECT ... MVCC 一致性读
SELECT ... WHERE ... 读历史版本
SELECT COUNT(*) 读快照
SELECT ... LIMIT 快照读
SELECT ... JOIN ... 快照读

📌 前提条件

  • 隔离级别 = RC / RR
  • 不是 FOR UPDATE / LOCK IN SHARE MODE

❌ 一定会加锁的情况(当前读)

SQL 加什么锁
SELECT ... FOR UPDATE 行 X + 表 IX
SELECT ... LOCK IN SHARE MODE 行 S + 表 IS
UPDATE ... 行 X + 表 IX
DELETE ... 行 X + 表 IX
INSERT ... 行 X + 表 IX

👉 只要是"当前读" = 一定加锁


四、最关键的概念:快照读 vs 当前读

1️⃣ 快照读(Snapshot Read)

sql 复制代码
SELECT * FROM user WHERE id = 1;
  • 读的是 历史版本
  • 不关心最新数据
  • 不加锁
  • 不阻塞任何人

✅ 默认 SELECT 都是 快照读


2️⃣ 当前读(Current Read)

sql 复制代码
SELECT * FROM user WHERE id = 1 FOR UPDATE;
  • 必须读 最新版本
  • 必须保证别人不能改
  • 所以 一定加锁

五、用一个时间线彻底看懂

初始数据

text 复制代码
id=1, balance=100

事务 A(先开始)

sql 复制代码
START TRANSACTION;
SELECT balance FROM account WHERE id = 1;
  • 读到:100
  • ❌ 不加锁

事务 B(后开始)

sql 复制代码
START TRANSACTION;
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
  • 改成 200
  • 加 X 锁 → 提交释放

事务 A 再查一次

sql 复制代码
SELECT balance FROM account WHERE id = 1;
在 RR 下:
  • 仍然读到:100
  • 因为 Read View 不变
  • ❌ 不加锁
在 RC 下:
  • 读到:200
  • 每次 SELECT 生成新 Read View
  • ❌ 不加锁

📌 关键点

不管 RC 还是 RR,普通 SELECT 都不加锁


六、那"什么时候会偷偷加锁"?(非常容易踩坑)

⚠️ 场景 1:SELECT ... FOR UPDATE

sql 复制代码
SELECT * FROM user WHERE age > 20 FOR UPDATE;
  • 加:

    • 行 X 锁
    • Next-Key Lock(行 + 间隙)
  • 表上加 IX

👉 范围查询 = 锁一大片


⚠️ 场景 2:唯一索引 vs 非唯一索引

sql 复制代码
SELECT * FROM user WHERE email='a@b.com' FOR UPDATE;
索引情况
唯一索引 行锁
非唯一索引 Next-Key Lock

⚠️ 场景 3:SERIALIZABLE 隔离级别

sql 复制代码
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM user WHERE id = 1;

😱 即使是普通 SELECT:

  • 也会加 S 锁
  • 等价于 LOCK IN SHARE MODE

📌 这是唯一一个"普通 SELECT 也加锁"的情况


七、意向锁 & MVCC 的关系

复制代码
快照读(普通 SELECT)
   ↓
不加行锁
   ↓
也就不需要意向锁

当前读(FOR UPDATE / UPDATE)
   ↓
加行锁
   ↓
自动加 IS / IX

八、总结

"普通查快照,不锁;
改数据、要最新,必锁;
FOR UPDATE / SERIALIZABLE,锁必到。"

相关推荐
SEO-狼术4 小时前
Detect Aurora PostgreSQL Issues Faster
数据库·postgresql
2501_945423544 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
樹JUMP4 小时前
Python虚拟环境(venv)完全指南:隔离项目依赖
jvm·数据库·python
用什么都重名4 小时前
Redis 入门与实践:从基础到 Stream 消息队列
数据库·redis·缓存
Mistra丶4 小时前
记一次 JVM+Postgresql的 “死锁” 问题排查
jvm·数据库·postgresql·死锁
一然明月4 小时前
Qt QML 锚定(Anchors)全解析
java·数据库·qt
分享牛5 小时前
Operaton入门到精通23-Operaton 2.0 原生支持 JUnit 6 核心指南
数据库·junit
编码忘我5 小时前
mysq系列之事务
数据库
知识分享小能手5 小时前
Redis入门学习教程,从入门到精通,Redis进阶编程知识点详解(5)
数据库·redis·学习
MekoLi295 小时前
MongoDB 新手完全指南:从入门到精通的实战手册
数据库·后端