普通select默认不加锁,仅在显式使用for update或lock in share mode时加锁;若查询条件未走索引,则行锁退化为全表锁定。

默认情况下,普通 SELECT 查询不会加任何锁(InnoDB 引擎下),但是否加锁取决于隔离级别、查询类型和是否显式声明锁定读。
普通 SELECT 什么时候会隐式加锁?
在 READ COMMITTED 或更低隔离级别下,普通 SELECT 是一致性非锁定读(基于 MVCC 快照),不加锁;但在 REPEATABLE READ 下,虽然仍走 MVCC,但某些场景(如唯一索引等值查询后执行 UPDATE)可能触发隐式加锁。真正决定“是否加锁”的关键不是隔离级别本身,而是你有没有写 FOR UPDATE 或 LOCK IN SHARE MODE。
-
SELECT * FROM user WHERE id = 1;→ 不加锁(MVCC 快照读) -
SELECT * FROM user WHERE id = 1 FOR UPDATE;→ 加 X 锁(排他锁),阻塞其他写和加锁读 -
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;→ 加 S 锁(共享锁),允许其他读,阻塞写
为什么加了 FOR UPDATE 还会锁表而不是行锁?
根本原因是查询条件没走索引。InnoDB 的行锁本质是“锁索引”,不是锁数据行。如果 WHERE 条件列没有索引(或用了函数、类型隐式转换导致索引失效),InnoDB 就无法精确定位记录,只能退化为锁全表(准确说是锁聚簇索引的所有叶子节点,效果等同于表级锁)。
- ✅ 正确:主键/唯一索引字段直接等值查询 → 加 Record Lock(记录锁)
- ✅ 正确:有索引的范围查询(如
id > 5 AND id )→ 加 Next-Key Lock(临键锁 = 记录锁 + 间隙锁) - ❌ 危险:
SELECT * FROM user WHERE name = 'Alice' FOR UPDATE;,而name列无索引 → 全表扫描 + 表级锁,高并发下极易死锁或雪崩
事务未提交时,锁到底持有多久?
锁持续到事务结束(COMMIT 或 ROLLBACK),不是语句执行完就释放。这是很多人踩坑的根源:开了事务、执行了 SELECT ... FOR UPDATE、忘了 COMMIT,结果锁一直挂着,后续所有相关操作全被阻塞。
- 手动开启事务后必须配对
COMMIT/ROLLBACK,否则锁不释放 - Spring 中用
@Transactional注解时,方法正常返回才自动COMMIT;抛异常且未被捕获,才自动ROLLBACK - JMeter 压测时若只发请求不 commit,很快就会出现
Lock wait timeout exceeded或Deadlock found when trying to get lock
乐观锁 vs 悲观锁,该选哪个?
别被名字迷惑——MySQL 本身不提供“乐观锁”功能,所谓乐观锁是应用层实现的机制(比如用 version 字段做更新校验)。而 FOR UPDATE 是典型的悲观锁,适合写多读少、冲突概率高的场景(如库存扣减、签到防重);乐观锁适合读多写少、冲突概率低的场景(如用户资料轻量修改)。
- 悲观锁实操成本低,但并发高时容易锁竞争甚至死锁
- 乐观锁要改表结构(加 version/timestamp)、改 SQL(
UPDATE ... SET ver=ver+1 WHERE id=? AND ver=?)、处理 CAS 失败重试逻辑 - 两者不是互斥方案:可以先用
FOR UPDATE查+锁+判重,再用乐观锁做最终更新,兼顾安全与性能
最常被忽略的一点:空结果集也会加锁。比如 SELECT * FROM reward WHERE uid='123' AND ctime > '2025-12-29' FOR UPDATE; 查不到数据,InnoDB 仍会在对应索引间隙上加 Gap Lock,防止其他事务插入“本该命中”的记录——这既是防幻读的保障,也是死锁的温床。










