为什么你的 SELECT 有时会阻塞?

上篇「插入意向锁」时为了产生间隙锁、插入意向锁时使用了 'SELECT . . . FOR UPDATE', 但你有没有想过:为什么有时候一个普通的 SELECT 不加锁,而加上 FOR UPDATE 就会阻塞别人? 我们就来进一步理解:快照读(Snapshot Read)当前读(Current Read)

一、基本概念:MySQL 的两种"读"法

在 Mysql 中,并非所有 SELECT 都一样。根据是否加锁、是否读最新数据,分为两类:

快照读(Snapshot Read)

  • 定义 :读取事务开始时(或语句开始时)的一致性视图(Read View),不加任何锁
  • 典型语句
sql 复制代码
  SELECT * FROM orders WHERE user_id = 1001;
  • 底层机制 :基于 MVCC(多版本并发控制),通过 undo log 构建历史版本。
  • 特点
    • 读不阻塞写,写不阻塞读;
    • 在 RR 隔离级别下,整个事务看到同一快照(可重复读);
    • 在 RC 级别下,每次 SELECT 都生成新快照(不可重复读)。

当前读(Current Read)

  • 定义 :读取数据库中最新的、已提交的数据 ,并加锁以防止其他事务修改。
  • 典型语句
sql 复制代码
  SELECT * FROM orders WHERE user_id = 1001 FOR UPDATE;        -- 排他锁
  SELECT * FROM orders WHERE user_id = 1001 LOCK IN SHARE MODE; -- 共享锁(MySQL 8.0+ 可用 FOR SHARE)
  UPDATE orders SET status = 'paid' WHERE id = 123;
  DELETE FROM orders WHERE id = 123;
  • 底层机制 :直接访问聚簇索引或二级索引的最新记录,并根据隔离级别加 记录锁 / 间隙锁(RR) / Next-Key Lock
  • 特点
    • 会阻塞其他写操作(甚至读操作,取决于锁类型)
    • 是实现"悲观锁"和防止并发冲突的关键手段。

快照读 = 安静地看历史;当前读 = 大声宣布"我要改这里,请别动!"


二、使用场景:什么时候该用哪种读?

场景 1:只读查询 → 用快照读

  • 用户查看订单列表、商品详情等;
  • 对数据一致性要求不高,或能接受"稍旧"数据;
  • 优势:零锁开销,高并发无压力。

场景 2:先查后改(Check-Then-Act)→ 必须用当前读!

java 复制代码
// 伪代码:错误示范(快照读)
Order order = select("SELECT * FROM orders WHERE id = 123"); // 快照读
if (order.status == "unpaid") {
    update("UPDATE orders SET status = 'paid' WHERE id = 123");
}

问题 :两个线程同时执行,都看到 status=unpaid,导致重复支付!

正确做法(当前读):

sql 复制代码
-- 加锁读取最新状态(仅用来体现当前读的作用,高并发场景下不建议使用 FOR UPDATE)
SELECT * FROM orders WHERE id = 123 FOR UPDATE;
-- 再判断并更新

场景 3:防止幻读(RR 级别下)

  • 业务要求"范围内不能有新数据插入",如库存扣减、唯一编号生成;
  • 必须用 SELECT ... FOR UPDATE 触发 Next-Key Lock(记录 + 间隙锁)
  • 否则即使快照读看不到新数据,别人仍可插入,破坏业务逻辑。

三、避坑指南

问题 1:FOR UPDATE 导致大量阻塞甚至死锁

  • 原因:范围查询未走索引,InnoDB 在 RR 下对主键全表加间隙锁;
  • 案例
sql 复制代码
  SELECT * FROM orders WHERE create_time > '2024-01-01' FOR UPDATE; -- create_time 无索引

→ 锁住整个表,所有 INSERT 被阻塞! 解决方案

  • 确保 WHERE 条件命中索引;
  • 高并发场景考虑降级到 READ COMMITTED(只加记录锁,不加间隙锁);
  • 避免大范围扫描,改用分页或等值查询。

问题 2:快照读 + 当前读混合,误判"幻读"

  • 现象
SQL 复制代码
  -- 事务内
  SELECT COUNT(*) FROM t WHERE id > 10;               -- 快照读,返回 0
  SELECT COUNT(*) FROM t WHERE id > 10 FOR UPDATE;   -- 当前读,返回 1!
  • 误解:"RR 下怎么还有幻读?"
  • 真相:这不是幻读!快照读看历史,当前读看现在,两者本就不该一致。

解决方案

  • 统一读取模式:要么全用快照读(接受只读一致性),要么关键路径全用当前读;
  • 不要用快照读做业务判断后再用当前读更新。

问题 3:RC 级别下 FOR UPDATE 无法防止幻读

  • 现象 :在 RC 下,即使用了 FOR UPDATE,别人仍可插入新数据;
  • 原因:RC 不使用间隙锁,只锁已有记录;
  • 影响:如"查无此用户 → 插入"可能失败(唯一键冲突)。

解决方案

  • 依赖 数据库唯一索引 作为最终兜底;
  • 应用层做好异常捕获与重试(如捕获 Duplicate entry);
  • 核心链路若需强一致性,保留 RR + 精准加锁。

快照读和当前读,是 InnoDB 实现高性能与一致性平衡的双翼。

  • 快照读 让读操作如丝般顺滑;
  • 当前读 为写操作筑起安全防线。

但在实际开发中,最大的风险不是技术本身,而是"不知道自己在用哪种读"

下次当你写下 SELECT 时,不妨多想一步 "我需要的是历史快照,还是此刻的真实?"


💡 感谢你看完这篇内容,这是我自己在工作学习中遇到的case,做一些简单的研究,并总结经验,如有遗漏或不合理的地方,欢迎你提出问题,让我们一起探索


相关推荐
程序新视界26 分钟前
为什么不建议基于Multi-Agent来构建Agent工程?
人工智能·后端·agent
做cv的小昊31 分钟前
【TJU】信息检索与分析课程笔记和练习(7)数据库检索—Ei
数据库·笔记·学习·全文检索
Victor35638 分钟前
Hibernate(29)什么是Hibernate的连接池?
后端
Victor35638 分钟前
Hibernate(30)Hibernate的Named Query是什么?
后端
zgl_200537791 小时前
ZGLanguage 解析SQL数据血缘 之 标识提取SQL语句中的目标表
java·大数据·数据库·数据仓库·hadoop·sql·源代码管理
莳花微语1 小时前
记录一次OGG进程abended,报错OGG-01431、OGG-01003、OGG-01151、OGG-01296问题的处理
数据库·sql·mysql
源代码•宸1 小时前
GoLang八股(Go语言基础)
开发语言·后端·golang·map·defer·recover·panic
czlczl200209251 小时前
OAuth 2.0 解析:后端开发者视角的原理与流程讲解
java·spring boot·后端
尋有緣1 小时前
力扣1355-活动参与者
大数据·数据库·leetcode·oracle·数据库开发
颜淡慕潇1 小时前
Spring Boot 3.3.x、3.4.x、3.5.x 深度对比与演进分析
java·后端·架构