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

相关推荐
资深web全栈开发2 分钟前
PostgreSQL枚举还是字符串:ENUM vs VARCHAR + CHECK 的权衡
数据库·postgresql
凯子坚持 c15 分钟前
C++基于微服务脚手架的视频点播系统---客户端(4)
数据库·c++·微服务
OceanBase数据库官方博客21 分钟前
OceanBase场景解码系列三|OB Cloud 如何稳定支撑中企出海实现数 10 倍的高速增长?
数据库·oceanbase·分布式数据库
m0_5613596731 分钟前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
山岚的运维笔记35 分钟前
SQL Server笔记 -- 第14章:CASE语句
数据库·笔记·sql·microsoft·sqlserver
Data_Journal41 分钟前
如何使用 Python 解析 JSON 数据
大数据·开发语言·前端·数据库·人工智能·php
ASS-ASH43 分钟前
AI时代之向量数据库概览
数据库·人工智能·python·llm·embedding·向量数据库·vlm
xixixi777771 小时前
互联网和数据分析中的核心指标 DAU (日活跃用户数)
大数据·网络·数据库·数据·dau·mau·留存率
crossaspeed2 小时前
MySQL-索引
mysql
范纹杉想快点毕业2 小时前
状态机设计与嵌入式系统开发完整指南从面向过程到面向对象,从理论到实践的全面解析
linux·服务器·数据库·c++·算法·mongodb·mfc