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

相关推荐
冰清-小魔鱼1 小时前
各类数据存储结构总结
开发语言·数据结构·数据库
深藏bIue1 小时前
MongoDB 4.4.30安装、数据迁移
数据库·mongodb
benyuanone1 小时前
MySQL环境项目迁移成国产化达梦环境
数据库·mysql
北凉军2 小时前
java连接达梦数据库,用户名是其他库的名称无法指定库,所有mapper查询的都是以用户名相同的库内的表
java·开发语言·数据库
尽兴-2 小时前
MySQL索引优化:从理论到实战
数据库·mysql·优化·b+树·索引·最左前缀
ZKNOW甄知科技2 小时前
IT自动分派单据:让企业服务流程更智能、更高效的关键技术
大数据·运维·数据库·人工智能·低代码·自动化
小光学长2 小时前
基于Web的长江游轮公共服务系统j225o57w(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库
Davina_yu3 小时前
2026年节假日表SQL
数据库·sql
是娇娇公主~3 小时前
工厂模式详细讲解
数据库·c++
天码-行空4 小时前
Linux 系统 MySQL 8.0 详细安装教程
linux·运维·mysql