为什么你的 SELECT 有时会阻塞?
|
zhenglin
2026年1月8日 15:21
本文热度 385
|
一、基本概念:MySQL 的两种“读”法
在 Mysql 中,并非所有 SELECT 都一样。根据是否加锁、是否读最新数据,分为两类:
快照读(Snapshot Read)
SELECT * FROM orders WHERE user_id = 1001;
当前读(Current Read)
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;
快照读 = 安静地看历史;当前读 = 大声宣布“我要改这里,请别动!”
二、使用场景:什么时候该用哪种读?
场景 1:只读查询 → 用快照读
用户查看订单列表、商品详情等;
对数据一致性要求不高,或能接受“稍旧”数据;
优势:零锁开销,高并发无压力。
场景 2:先查后改(Check-Then-Act)→ 必须用当前读!
// 伪代码:错误示范(快照读)
Order order = select("SELECT * FROM orders WHERE id = 123"); // 快照读
if (order.status == "unpaid") {
update("UPDATE orders SET status = 'paid' WHERE id = 123");
}
问题:两个线程同时执行,都看到 status=unpaid,导致重复支付!
正确做法(当前读):
-- 加锁读取最新状态(仅用来体现当前读的作用,高并发场景下不建议使用 FOR UPDATE)
SELECT * FROM orders WHERE id = 123 FOR UPDATE;
-- 再判断并更新
场景 3:防止幻读(RR 级别下)
业务要求“范围内不能有新数据插入”,如库存扣减、唯一编号生成;
必须用 SELECT ... FOR UPDATE 触发 Next-Key Lock(记录 + 间隙锁);
否则即使快照读看不到新数据,别人仍可插入,破坏业务逻辑。
三、避坑指南
问题 1:FOR UPDATE 导致大量阻塞甚至死锁
SELECT * FROM orders WHERE create_time > '2024-01-01' FOR UPDATE; -- create_time 无索引
锁住整个表,所有 INSERT 被阻塞! 解决方案:
问题 2:快照读 + 当前读混合,误判“幻读”
-- 事务内
SELECT COUNT(*) FROM t WHERE id > 10; -- 快照读,返回 0
SELECT COUNT(*) FROM t WHERE id > 10 FOR UPDATE; -- 当前读,返回 1!
解决方案:
问题 3:RC 级别下 FOR UPDATE 无法防止幻读
解决方案:
快照读和当前读,是 InnoDB 实现高性能与一致性平衡的双翼。
但在实际开发中,最大的风险不是技术本身,而是“不知道自己在用哪种读”。
下次当你写下 SELECT 时,不妨多想一步 “我需要的是历史快照,还是此刻的真实?”
参考文章:原文链接
该文章在 2026/1/8 15:23:12 编辑过