目录
前言
MySQL读取(主要指 InnoDB 存储引擎)中“快照读(snapshot read,也叫一致性读/consistent read)”和“当前读(current read)”。
如下所示:
这两种读取方式在事务隔离级别、并发控制和数据一致性方面有着本质的区别。主要区别在于是否加锁以及数据一致性。
1、MySQL读取定义
1.1、锁的分类
1、共享锁(S锁):
共享 (S) 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。
如果事务T仅对数据A进行读取,那么会对数据A加上共享锁,之后则其他事务如果要读取数据A的话可以对其继续加共享锁,但是不能加排他锁(也就是无法修改数据)。获准共享锁的事务只能读数据,不能修改数据。
2、排他锁(X锁):
用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
如果事务T对数据A要进行修改,则需要对其添加排它锁,加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
SQL代码示例:
共享锁
select * from table id = 1 lock in share mode;
排他锁
select * from table where id = 1 for update;
1.2、快照读与当前读
1、快照读
(Snapshot Read / Consistent Read):基于事务快照(MVCC),读取的是“某个时间点”的已提交版本,不加锁、非阻塞,多个读返回一致的历史版本。
REPEATABLE READ 下(基于事务开始时的快照):同一事务内多次 SELECT 返回相同结果;
READ COMMITTED 下每个语句建立新的快照。
SQL示例如下:
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 1; -- 快照读,读取事务开始时的数据
-- 事务 B
START TRANSACTION;
UPDATE users SET name = 'Alice' WHERE id = 1; -- 更新数据并提交
COMMIT;
-- 事务 A
SELECT * FROM users WHERE id = 1; -- 仍然读取事务开始时的数据(快照读)
COMMIT;
2、当前读
(Current Read):直接读取当前最新数据(buffer pool/磁盘)并且读取之后还要保证其他并发事务不能修改当前记录,对读取的记录加锁。
当前读:select…lock in share mode,select…for update。
当前读:update,delete,insert。
SQL代码示例如下:
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 当前读,获取最新数据并加锁
-- 事务 B
START TRANSACTION;
UPDATE users SET name = 'Alice' WHERE id = 1; -- 被阻塞,等待事务 A 释放锁
-- 事务 A
COMMIT; -- 提交后,事务 B 的更新操作继续执行
1.3、使用场景
1、当前读适用场景
- 需要获取最新数据的实时查询
- 需要保证数据一致性的金融交易
- 先读后写的业务逻辑
- 需要防止并发修改的场景
- 只读查询(报表、数据分析)
- 对实时性要求不高的查询
- 大查询,不希望阻塞其他操作
- 需要高并发的读场景
1.4、区别
如下所示:
2、实现机制
2.1、实现原理
1、快照读:通过MVCC+undolog实现;
如果最新版本不是当前事务可见,InnoDB 会从 undo log 找到符合快照时间点的版本(所以快照读可能读取 undo 中的旧版本)。不会设置锁。
不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
所以快照读都是去读取undolog中链首的最新的旧记录。
2、当前读:通过共享锁+排他锁+Next-Key Lock实现;
当前读通过next-key锁(记录锁+间隙锁)来实现:
行锁(Record Lock):锁定索引记录
间隙锁(Gap Lock):锁定索引记录之间的间隙
next-key锁:行锁+间隙锁,锁定一个范围并锁定记录本身
例如执行DELETE FROM T WHERE age = 7;时,MySQL会在age索引上加锁(4,10)区间,防止其他事务
插入age=7的记录,从而避免幻读问题。
如下所示:
- 每次对行数据进行读取的时候,加共享锁。此时就不允许修改,但是允许其他事务读取,所以每次都可以读到最新的数据。
- 每次对行数据进行修改的时候,加排他锁,不允许其他事务读取和修改。这种情况下其他事务读取的数据也一定是最新的数据。
- 每次对范围行数据进行读取的时候,对这个范围加一个范围共享锁。
- 每次对范围行数据进行修改的时候,读这个范围加一个范围排它锁。
- 基于上述锁机制,实现当前读,确保每次读取的都是最新的数据。
⚠️注意:
next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁(而非数据行),间隙锁是加在索引之间的。
2.2、隔离级别和快照联系
1、隔离级别
1、读未提交(READ UNCOMMITTED)
快照读:不存在,总是读取最新数据(包括未提交的)。
当前读:读取最新已提交数据并加锁。
2、读已提交(READ COMMITTED)
快照读:每次SELECT都会生成新的ReadView,读取最新已提交数据。
当前读:读取最新已提交数据并加锁。
3、可重复读(REPEATABLE READ)
快照读:事务第一次SELECT时生成ReadView,后续复用,保证读取一致性。
当前读:读取最新已提交数据并加锁。
4、串行化(SERIALIZABLE)
快照读:退化为当前读(加共享锁)。
当前读:读取最新已提交数据并加锁。
2、快照读
REPEATABLE READ(默认):
事务开始时建立一次快照,整个事务内普通 SELECT 都基于这个快照(repeatable)。
READ COMMITTED:
每个语句单独建立快照(statement-level),因此同一事务内不同语句可能看到不同已提交数据(避免脏读,但允许不可重复读)。
Undo log 提供历史版本;如果旧版本被 purge 掉,读取老快照可能失败。
2.3、快照何时生成
1、在读未提交隔离级别下,快照是什么时候生成的?
没有快照,因为不需要,怎么读都读到最新的,不管是否提交。
2、在读已提交隔离级别下,快照是什么时候生成的?
SQL语句开始执行的时候。
3、在可重复读隔离级别下,快照是什么时候生成的?
事务开始的时候(可能会有很多条select SQL语句执行,快照生命周期是到事务结束的时候)
4、在串行化隔离级别下,快照是什么时候生成的?
“写”会加“写锁”,“读”会加“读锁”,读的的数据都是当前最新的数据(没有快照,当前读)
3、SQL场景实现
3.1、快照读
1、 快照读不会看到后续提交(REPEATABLE READ):事务级快照读
事务 T1:
BEGIN; SELECT value FROM t WHERE id=1;
-- 假设值为 A (从快照读)(此时 T1 未提交,继续执行)
事务 T2:
BEGIN; UPDATE t SET value='B' WHERE id=1; COMMIT;
回到 T1:在 REPEATABLE READ 下仍然看到 A(快照不变)
SELECT value FROM t WHERE id=1;
COMMIT;
2、 READ COMMITTED 下快照行为(每语句快照):语句级快照读
事务 T1:
BEGIN; SELECT value FROM t WHERE id=1;
-- 看到 A
事务 T2:
BEGIN; UPDATE t SET value='B' WHERE id=1; COMMIT;
T1 再次执行:
SELECT value FROM t WHERE id=1;
-- 在 READ COMMITTED 下可能看到 B(新的语句快照)
3.2、当前读
1、当前读会立即看到并与写冲突/加锁:
事务 T1:
BEGIN; SELECT value FROM t WHERE id=1 FOR UPDATE;
-- 当前读并对该行加排它锁(此时 T1 锁住该行)
事务 T2(并发执行):
BEGIN; UPDATE t SET value='B' WHERE id=1; COMMIT;
-- 将在 T1 提交前被阻塞,直到 T1 提交或回滚
4、锁的细节(与当前读相关)
1、SELECT ... FOR UPDATE:
对选中的记录加排它锁(record lock),在 REPEATABLE READ 下通常是 next‑key lock(record+gap),能防幻读;在 READ COMMITTED 下可能只加 record lock(实现差异)。
2、SELECT ... LOCK IN SHARE MODE:
共享锁,允许并发读取但阻止写入。
UPDATE/DELETE 语句本质是当前读(读取并锁定匹配行),而非快照读。
5、影响 / 并发行为
5.1、快照读:
不阻塞其他事务的写操作(不会加锁)。
不被子后续提交所影响(在 REPEATABLE READ 内)不会阻塞写者,但写者提交不会让已存在事务的快照变更。
5.2、当前读:
会加锁,可能阻塞其他事务(或被其他事务阻塞),用于实现悲观锁定和防止幻读/并发冲突。SELECT ... FOR UPDATE 会锁定读取到的记录(并可能使用 next-key lock 以避免幻读,取决于隔离级别和索引类型)。
6、注意事项与常见误区
1、 “快照读不会造成任何索引访问” 不准确:
快照读也会走索引来定位行,但读取逻辑是基于版本可见性(从 undo 中回溯),它不设置锁。
2、长事务会保留较多 undo 数据:
影响 undo log 大小并增加 purge 压力;长时间的快照读(长事务)会阻止旧版本被清理。
3、不稳定结果
与快照/当前读无直接关系,但与隔离级别和锁有关,使用 skip/limit 的分页在并发写入下可能产生。
总结
普通 SELECT(无 FOR UPDATE/LOCK)是快照读(基于 MVCC,非锁定),在 REPEATABLE READ 下为事务级快照,同一事务内多次 SELECT 返回一致结果;READ COMMITTED 下每语句建立快照。
SELECT ... FOR UPDATE / UPDATE / DELETE 等是当前读,会读取最新数据并加锁,可能阻塞其他事务写入。
选择隔离级别与是否加锁,需要在“数据一致性需求(可重复读/防止幻读)”与“并发性能(锁竞争/阻塞)”之间权衡。
参考文章: