一文讲透 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,锁必到。"

相关推荐
周小董4 分钟前
[1369]Windows上安装PostgreSQL
数据库·windows·postgresql
一只积极向上的小咸鱼2 小时前
第一性原理思考:从拆解问题到用实验检验答案
数据库
这个DBA有点耶3 小时前
时序数据库InfluxDB高基数问题深度拆解:内存索引膨胀机制与OOM完整链路
数据库·架构·dba
SL_staff3 小时前
运维救火实录:钉钉考勤API变更引发工资计算异常,我们用JVS-Logic可视化逻辑引擎实现分钟级热修复
java·mysql·全栈
加油yx3 小时前
MySQL数据类型
数据库·mysql
蓝胖的四次元口袋3 小时前
Redis知识梳理(1)
数据库·redis·缓存
夜雪一千3 小时前
MySQL 数据加密实战:从传输、存储到字段级加密,分清场景不踩坑
数据库·mysql
wefg14 小时前
【Redis】C++ 自定义客户端
数据库·redis·缓存
这个DBA有点耶4 小时前
为什么UPDATE比SELECT危险得多?锁范围等于扫描行数
数据库·架构·代码规范
天国梦5 小时前
国密门禁品牌怎么选?2026年最新3个避坑要点
运维·服务器·数据库·python