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

相关推荐
TG:@yunlaoda360 云老大2 小时前
华为云国际站代理商GES的应用场景有哪些?
服务器·数据库·华为云
010不二2 小时前
基于Appium爬虫文本导出可话个人动态
数据库·爬虫·python·appium
火山引擎开发者社区2 小时前
云数据库 MySQL 2025 运维革新:大版本升级无忧+蓝绿零停机+存储自动扩容全覆盖
运维·数据库·mysql
白帽黑客-晨哥2 小时前
Web安全中SQL注入绕过WAF的具体手法和实战案例
sql·安全·web安全·职场和发展·渗透测试
杜子不疼.3 小时前
Spring AI 与向量数据库:构建企业级 RAG 智能问答系统
数据库·人工智能·spring
山峰哥3 小时前
Python爬虫实战:从零构建高效数据采集系统
开发语言·数据库·爬虫·python·性能优化·架构
_OP_CHEN4 小时前
【C++数据结构进阶】从B + 树 / B * 树到数据库索引:B树的进化之路与 MySQL 实战解析
数据结构·数据库·b树·mysql·innodb·b+树·mylsam
云老大TG:@yunlaoda36010 小时前
华为云国际站代理商TaurusDB的成本优化体现在哪些方面?
大数据·网络·数据库·华为云
TG:@yunlaoda360 云老大10 小时前
华为云国际站代理商GeminiDB的企业级高可用具体是如何实现的?
服务器·网络·数据库·华为云