select for update只锁实际扫描命中的行(需走索引),否则锁全表;必须显式开启事务才能保证锁持续到后续操作;无索引时锁聚簇索引所有记录,空查询也可能加间隙锁。

SELECT FOR UPDATE 在事务里到底锁什么
它只锁住 SELECT 实际扫描并命中的行(InnoDB 下),不是整张表,也不是所有索引范围 —— 前提是查询能走索引。如果 WHERE 条件没命中索引,InnoDB 会退化为锁全表(更准确说是锁聚簇索引的所有记录),并发性能直接崩掉。
- 有主键或唯一索引:锁对应单行(或几行)
- 有普通索引但不是覆盖索引:可能触发“间隙锁”或“临键锁”,锁住值区间,防幻读
- 全表扫描或
WHERE无索引:锁所有聚簇索引记录,等效于表级写锁 - 查询返回空结果:仍可能加间隙锁(比如
WHERE id = 100但 100 不存在,会锁 (99,101) 这个间隙)
不显式开启事务,SELECT FOR UPDATE 会怎样
MySQL 默认 autocommit=1,此时 SELECT FOR UPDATE 会自动开启一个隐式事务,执行完就提交 —— 锁只存在那一条语句的执行期间,根本起不到“保护后续更新”的作用。
- 常见误用:
SELECT FOR UPDATE单独执行,接着UPDATE另起一个语句 → 两个语句不在同一事务,锁早释放了 - 正确做法:必须手动
BEGIN或START TRANSACTION,再执行SELECT FOR UPDATE,最后COMMIT或ROLLBACK - 注意:
SET autocommit = 0后所有语句都在同一事务里,但容易忘记COMMIT,导致长事务和锁堆积
为什么 SELECT FOR UPDATE 有时卡住、有时报 Lock wait timeout
本质是等待其它事务持有的锁释放。卡住时间取决于 innodb_lock_wait_timeout(默认 50 秒),超时就报错 Lock wait timeout exceeded。
- 典型场景:事务 A 拿着某行锁未提交,事务 B 对同一行执行
SELECT FOR UPDATE→ B 阻塞直到超时 - 死锁检测会主动干掉其中一个事务,报错
Deadlock found when trying to get lock,不是超时 - 避免卡死:应用层加超时控制(如 JDBC 的
setQueryTimeout),或改用SELECT FOR UPDATE NOWAIT(MySQL 8.0.1+),立刻报错而不是等 - 查谁在堵你:
SELECT * FROM information_schema.INNODB_TRX看当前事务,再连INNODB_LOCK_WAITS和INNODB_LOCKS(8.0 已移除后者)定位阻塞链
UPDATE 前真有必要先 SELECT FOR UPDATE 吗
不一定。如果 UPDATE 本身带明确 WHERE 条件(且命中索引),InnoDB 会在更新前自动对目标行加行锁 —— 此时先 SELECT FOR UPDATE 是冗余的,还多一次网络往返和锁获取开销。
- 需要先查后更:当业务逻辑依赖“读出的旧值”做判断(比如余额不能为负),必须用
SELECT FOR UPDATE把读和后续操作包进同一事务 - 不需要先查:纯条件更新,如
UPDATE account SET balance = balance - 100 WHERE id = 123,InnoDB 自动加锁,安全 - 注意并发更新丢失:如果多个事务都基于旧值计算新值(比如读出 balance=100,都算出 0 并写回),仍需
SELECT FOR UPDATE或改用原子操作(UPDATE ... SET x = x + 1)
真正容易被忽略的是间隙锁行为 —— 它不锁数据行,却能阻止插入,这种“看不见的锁”在线上突然出现幻读或插入失败时最难排查。











