快照读和当前读
在 MySQL 中,数据读取方式主要分为 快照读 和 当前读,二者的核心区别在于是否依赖 MVCC(多版本并发控制)的历史版本、是否加锁,以及读取的数据版本是否为最新。以下是详细说明:
一、快照读(Snapshot Read)
定义
快照读是指读取 MVCC 机制保存的历史数据版本,通过事务启动时生成的一致性视图(Read View)获取数据,不加锁,因此不会阻塞其他事务的读写操作。
触发场景
所有 不加锁的普通 SELECT 语句 均为快照读,例如:
SELECT * FROM 表 WHERE id = 1;
SELECT name FROM 表 WHERE status = 'active';
核心特点
- 依赖隔离级别:
- 在 读已提交(Read Committed) 级别:每次 SELECT 都会生成新的 Read View,只能看到已提交的最新数据(避免脏读)。
- 在 可重复读(Repeatable Read) 级别:整个事务内使用同一个 Read View,多次读取结果一致(避免不可重复读)。
- 无锁阻塞:读取时不申请任何锁,不影响其他事务的修改。
- 不读取未提交数据:只能看到符合隔离级别的历史提交版本,看不到其他事务未提交的修改。
二、当前读(Current Read)
定义
当前读是指读取数据的 最新版本(无视 MVCC 历史版本),且读取时会对目标行 加锁(共享锁或排他锁),以保证数据修改的原子性和一致性。
触发场景
所有 需要获取最新数据并加锁的操作 均为当前读,包括:
写操作:
INSERT
、UPDATE
、DELETE
- 执行这些操作时,会先读取最新数据版本,然后对目标行加 排他锁(X 锁),防止其他事务同时修改。
- 例如:
UPDATE 表 SET name = 'b' WHERE id = 1;
会先当前读id=1
的最新行,加 X 锁后修改。加锁的 SELECT 语句:
SELECT ... FOR SHARE
(或SELECT ... LOCK IN SHARE MODE
):加 共享锁(S 锁),允许其他事务读,但阻止其他事务加排他锁。SELECT ... FOR UPDATE
:加 排他锁(X 锁),阻止其他事务加共享锁或排他锁。核心特点
- 读取最新版本:直接读取当前内存中已提交或未提交的最新数据(即使其他事务未提交,也能看到其修改)。
- 加锁阻塞:会对目标行加锁,其他事务若需操作同一行,需等待锁释放(提交或回滚)。
- 保证数据一致性:常用于需要精确修改最新数据的场景(如并发更新),避免丢失更新。
三、其他特殊读取情况?
除了快照读和当前读,MySQL 中没有其他独立的读取方式,但需注意以下特殊场景:
串行化(Serializable)隔离级别下的读
- 在最高隔离级别 “串行化” 中,普通
SELECT
会被隐式转换为 当前读(加共享锁),以完全避免并发问题。此时快照读机制不生效,本质仍是当前读的一种特殊表现。也就是说在串行化隔离级别下,线程 B 会等待线程 A 提交或回滚后,读取到线程 A 提交后的数据或回滚前的原始数据。A没提交或没回滚时,线程B的读取被阻塞
DDL 操作中的读
- 执行
ALTER TABLE
等 DDL 语句时,会对表加 metadata 锁(MDL 锁),此时的读操作可能被阻塞,但读取方式仍属于快照读或当前读(取决于具体语句是否加锁)。总结:核心区别与场景表
读取方式 触发场景 是否加锁 读取的数据版本 典型操作示例 快照读 不加锁的普通 SELECT 不加锁 MVCC 历史提交版本 SELECT * FROM 表;
当前读 写操作(INSERT/UPDATE/DELETE)、加锁 SELECT 加锁(S 或 X 锁) 最新版本(含未提交) UPDATE 表 SET ...;
、SELECT ... FOR UPDATE;
MySQL 可重复读隔离级别下的事务锁与数据可见性
1. 核心场景
- 初始数据:表中某行
name = 'a'
。- 线程 A:开启事务,执行
UPDATE 表 SET name = 'b' WHERE name = 'a'
(未提交)。- 线程 B:开启事务,执行
UPDATE 表 SET name = 'c' WHERE name = 'b'
。
- 如果线程B是
UPDATE 表 SET name = 'c' WHERE name = 'a',因为是当前读,所以name已经被线程A修改,所以找不到WHERE name = 'a',也就无事发生
2. 关键机制
当前读(Current Read)
UPDATE
/DELETE
/INSERT
/SELECT ... FOR UPDATE
会触发当前读,直接读取最新数据(无论是否提交)。- 与快照读的区别:普通
SELECT
使用事务启动时的一致性视图(MVCC),当前读无视视图,读取最新版本。排他锁(X 锁)
UPDATE
会对匹配的行加排他锁,阻止其他事务同时修改。- 锁持有至事务提交 / 回滚。
3. 执行流程
线程 A
- 触发当前读,匹配
name = 'a'
的行,加排他锁,将其修改为name = 'b'
(未提交)。- 锁未释放,数据仅存在于内存中。
线程 B
- 触发当前读,读取到线程 A 未提交的
name = 'b'
,匹配WHERE name = 'b'
。- 尝试加排他锁,因线程 A 已持有锁而进入阻塞状态,等待锁释放。
4. 结果取决于线程 A 的操作
情况分类 线程 A 操作 线程 B 操作流程 最终 name
值核心原因分析 情况 1 先提交 1. 阻塞结束,获取锁
2. 当前读匹配name = 'b'
,修改为'c'
3. 提交事务'c'
线程 A 的修改生效,线程 B 基于 'b'
进一步修改情况 2 先回滚 1. 阻塞结束,获取锁
2. 当前读发现name = 'a'
,条件不匹配,无修改
3. 提交事务'a'
线程 A 的修改撤销,线程 B 的 WHERE
条件不成立,未执行修改情况 3(不可能发生) 未提交 / 未回滚/(线程b先提交) 因未获取锁处于阻塞状态, UPDATE
未执行,无法提交事务无结果 线程 B 需等待线程 A 释放锁才能执行,无法抢先提交 5. 关键点总结
当前读的可见性
- 线程 B 的
UPDATE
能看到线程 A 未提交的修改(name = 'b'
),因此匹配条件并尝试加锁。锁的阻塞机制
- 线程 B 必须等待线程 A 释放锁,无法抢先提交事务。
事务原子性
- 线程 A 的提交 / 回滚决定最终数据的基础版本,线程 B 的修改依赖于此。
对比场景:若线程 B 查询
SELECT * FROM 表 WHERE name = 'b'
- 普通
SELECT
(快照读):看不到线程 A 未提交的修改,返回空结果。SELECT ... FOR UPDATE
(当前读):会阻塞等待线程 A 释放锁,锁释放后返回name = 'b'
(若 A 提交)或空(若 A 回滚)。6. 实践建议
- 避免长事务:长时间持有锁会增加阻塞概率。
- 优化查询条件:确保
UPDATE
的WHERE
条件使用索引,减少锁范围。- 处理阻塞异常:业务代码需考虑锁等待超时的情况(如设置
innodb_lock_wait_timeout
)。