[小技巧43]MySQL MVCC 深度解析:快照读 vs 当前读

一、快照读(Snapshot Read)与当前读(Current Read)

1.1 快照读(Snapshot Read)

快照读是指事务在执行 SELECT 查询时,读取的是事务启动时的数据快照。它不会阻塞其他写操作,也不会被其他写操作阻塞。这是因为快照读依赖于 MVCC 机制,通过读取历史版本来实现一致性视图。

特点:

  • 无锁:快照读不需要加任何锁。
  • 一致性视图:读取的是事务开始时的数据快照。
  • 适用场景:适用于大多数只读查询场景。

示例:

sql 复制代码
SELECT * FROM users WHERE id = 1;

1.2 当前读(Current Read)

当前读是指事务在执行某些特定查询时,需要获取最新的数据视图,因此会加锁以确保数据的一致性。

常见的当前读操作包括 SELECT ... FOR UPDATE 和 SELECT ... LOCK IN SHARE MODE。

特点:

  • 加锁:当前读需要加锁,防止其他事务修改数据。
  • 最新数据:读取的是当前时刻的最新数据。
  • 适用场景:适用于需要保证数据一致性的写操作或高并发场景下的读操作。

示例:

sql 复制代码
SELECT * FROM users WHERE id = 1 FOR UPDATE;

核心区别概览

特性 SELECT ... FOR UPDATE SELECT ... LOCK IN SHARE MODE
中文名 排他锁(写锁) 共享锁(读锁)
锁类型 X 锁(Exclusive Lock) S 锁(Shared Lock)
是否阻塞其他事务读? ❌ 不阻塞普通 SELECT(快照读) ✅ 阻塞其他 FOR UPDATE / LOCK IN SHARE MODE ❌ 不阻塞普通 SELECT ❌ 不阻塞其他 LOCK IN SHARE MODE ✅ 阻塞 FOR UPDATE
是否阻塞其他事务写? ✅ 阻塞所有写操作(UPDATE/DELETE) ✅ 阻塞写操作(因为写需要 X 锁)
典型用途 "我要修改这条数据,请别人别动" "我要确保这条数据不被改,但允许多人同时读"
隔离级别要求 所有隔离级别均生效 所有隔离级别均生效

一句话总结:

  • FOR UPDATE = "我独占,谁也别碰" (连"读锁"都不让别人加)
  • LOCK IN SHARE MODE = "你可以读,但不能改"(允许别人加同样的"读锁")

锁机制详解

1. SELECT ... FOR UPDATE(排他锁 / X 锁)
  • 对查询命中的每一行 加 X 锁(排他锁)
  • X 锁特性 :
    • 与其他任何锁(S 或 X)互斥
    • 同一时刻只能有一个事务持有某行的 X 锁
  • 效果 :
    • 其他事务无法对该行执行 UPDATE、DELETE、FOR UPDATE、LOCK IN SHARE MODE
    • 但普通 SELECT(快照读)仍可执行(因为不走当前读,不受锁影响)

示例:

sql 复制代码
-- 事务 A
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 加 X 锁
-- 此时事务 B 执行以下语句会阻塞:
--   UPDATE accounts SET balance = 100 WHERE id = 1;
--   SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
--   SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
-- 但以下语句**不会阻塞**:
--   SELECT * FROM accounts WHERE id = 1; -- 快照读,走 MVCC
2. SELECT ... LOCK IN SHARE MODE(共享锁 / S 锁)
  • 对查询命中的每一行 加 S 锁(共享锁)
  • S 锁特性 :
    • 与 S 锁兼容(多个事务可同时持有)
    • 与 X 锁互斥
  • 效果 :
    • 其他事务可以执行 LOCK IN SHARE MODE(并发读)
    • 但无法执行 FOR UPDATE、UPDATE、DELETE(因为这些需要 X 锁)

示例:

sql 复制代码
-- 事务 A
BEGIN;
SELECT * FROM products WHERE id = 100 LOCK IN SHARE MODE; -- 加 S 锁

-- 事务 B 可以:
SELECT * FROM products WHERE id = 100 LOCK IN SHARE MODE; -- 成功(S-S 兼容)

-- 事务 B 不能:
SELECT * FROM products WHERE id = 100 FOR UPDATE;        -- 阻塞(S-X 冲突)
UPDATE products SET price = 99 WHERE id = 100;           -- 阻塞(需要 X 锁)

关键注意事项

  1. 两者都属于"当前读",会绕过 MVCC
  • 它们读取的是最新已提交的数据(不是快照)
  • 即使在 REPEATABLE READ 隔离级别下,也会看到其他事务已提交的最新值
  1. 锁的范围取决于 WHERE 条件和索引
  • 如果 WHERE 条件能命中索引 → 只锁命中的行
  • 如果全表扫描 → 锁住所有扫描过的行(甚至间隙锁!)
  • 在 REPEATABLE READ 下,还会加 Next-Key Lock(行锁 + 间隙锁) 防止幻读

示例(RR 隔离级别):

sql 复制代码
-- 假设 id 是主键
SELECT * FROM t WHERE id = 5 FOR UPDATE; 
-- 只锁 id=5 这一行

-- 假设 name 无索引
SELECT * FROM t WHERE name = 'Alice' FOR UPDATE;
-- 可能锁住整个表(或大量无关行)!

MySQL 8.0+ 已废弃 LOCK IN SHARE MODE

  • 官方推荐使用 FOR SHARE 替代(语法更清晰):

    sql 复制代码
    SELECT * FROM t WHERE id = 1 FOR SHARE; -- 等价于 LOCK IN SHARE MODE
  • 但功能完全一致,旧语法仍支持。

使用场景对比

SELECT ... FOR UPDATE 适用场景

  • 资金转账 :确保账户余额在读取后不被他人修改

    sql 复制代码
    BEGIN;
    SELECT balance FROM accounts WHERE user_id = 1 FOR UPDATE;
    -- 检查余额足够
    UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
    COMMIT;
  • 库存扣减:防止超卖

  • 状态机更新:如订单状态变更

SELECT ... LOCK IN SHARE MODE 适用场景

  • 报表生成 :确保统计期间数据不被修改

    sql 复制代码
    BEGIN;
    SELECT SUM(amount) FROM orders 
    WHERE status = 'paid' LOCK IN SHARE MODE;
    -- 生成报表(其他人可读,但不能改订单状态)
    COMMIT;
  • 父子数据一致性校验:如检查订单与商品是否存在

但注意 :由于 S 锁容易引发死锁(多个事务互相持有 S 锁,再申请 X 锁),现代应用更倾向于用 FOR UPDATE + 业务逻辑优化 ,而非 LOCK IN SHARE MODE。

死锁风险对比

场景 FOR UPDATE LOCK IN SHARE MODE
死锁概率 中(典型:A→B, B→A) 高(S 锁 + 后续 X 锁易形成环)
典型案例 两个事务交叉更新两行 事务 A 持 S 锁想升级 X 锁,事务 B 也在等

建议 :除非明确需要"多人并发读+防写",否则优先使用 FOR UPDATE。

总结:如何选择?

你的需求 推荐语句
"我要读完就改,别让别人干扰" ✅ SELECT ... FOR UPDATE
"我只需要确保数据不被改,允许多人同时读" ⚠️ SELECT ... FOR SHARE(谨慎使用)
"我只是查一下,不需要锁" ✅ 普通 SELECT(走 MVCC 快照读)

终极建议:

  • 90% 的场景用 FOR UPDATE 足够
  • 尽量避免 LOCK IN SHARE MODE,除非你清楚其并发模型
  • 务必使用索引,避免意外锁表
  • 事务尽量短,减少锁持有时间

1.3 快照读 vs 当前读:对比表

特性 快照读(Snapshot Read) 当前读(Current Read)
加锁 无 是
数据一致性 事务启动时的快照 当前时刻的最新数据
并发性能 高 中等
常见操作 SELECT SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE

二、MVCC 仅适用于快照读,不适用于当前读

2.1 MVCC 的作用边界

MVCC 主要应用于快照读 ,通过为每个事务分配一个唯一的事务 ID(DB_TRX_ID),并维护多个版本的历史数据,使得不同事务可以读取到不同的数据版本。然而,MVCC 并不适用于当前读,因为当前读需要获取最新的数据视图,这通常涉及到加锁操作。

关键点:

  • 快照读:使用 MVCC 提供一致性视图。
  • 当前读:绕过 MVCC,直接访问最新数据并加锁。

三、总结

MVCC 是 MySQL InnoDB 实现高并发读写的基石,通过快照读提供了无锁的一致性视图,极大地提升了系统的并发性能。

然而,MVCC 并不适用于所有类型的读操作,特别是那些需要获取最新数据的当前读操作。

关键知识点:

  • 快照读:无锁,读取事务启动时的数据快照。
  • 当前读:加锁,读取当前时刻的最新数据。
  • MVCC:适用于快照读,不适用于当前读。
  • Undo Log:记录历史版本,支持多版本存储和事务回滚。

相关命令与工具

  1. 查看活跃事务:

    sql 复制代码
    SELECT trx_id, trx_state, trx_started 
    FROM information_schema.innodb_trx 
    ORDER BY trx_started DESC;
  2. 查看锁定信息(MySQL 8.0+):

    sql 复制代码
    SELECT * FROM performance_schema.data_locks;
相关推荐
Titan202411 分钟前
MySQL访问个人学习笔记
笔记·学习·mysql
邓工说电1 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
for_ever_love__1 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram
adinnet20261 小时前
企业微调大模型(Fine-tuning):先吃透 RAG 还是先训自己的模型
大数据·数据库
上位机妹子2 小时前
EF Core 两种继承映射:每层次一张表(TPH)与每类型一张表(TPT)
数据库·c#·.netcore·ef core
小马同学-2 小时前
Redis消息队列与客户端编程
数据库·redis
for_ever_love__2 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结
染指11103 小时前
140.Agent-多Agent框架-Agent执行Skills流程
数据库·人工智能·设计模式·langchain·agents
fengkai45453 小时前
十二、Redis -2
运维·数据库·redis·容器