SELECT FOR UPDATE 必须显式开启事务才生效,单独执行无效;WHERE条件必须命中索引,否则锁全表;UPDATE前通常无需前置SELECT,仅当依赖旧值判断或多步原子操作时才需加锁。

SELECT FOR UPDATE 必须显式开启事务才生效
单独执行 SELECT ... FOR UPDATE 不会加锁,MySQL 会静默忽略或报错(取决于 SQL mode),根本起不到保护作用。它只在已开启且未提交的事务中才真正持锁。
常见错误是写完 SELECT ... FOR UPDATE 就立刻执行 UPDATE,但没用 BEGIN 或 START TRANSACTION 包裹 —— 此时每个语句都是自动提交的独立事务,锁一查完就释放了。
- 检查当前会话是否自动提交:
SELECT @@autocommit;,返回1表示开启,需先SET autocommit = 0; - 推荐写法:始终用
BEGIN显式开启,COMMIT或ROLLBACK显式结束 - ORM 用户注意:确认框架事务配置(如 Spring 的
@Transactional)确实开启了数据库事务,而非仅应用层标记
WHERE 条件必须命中索引,否则锁全表
SELECT ... FOR UPDATE 锁的是索引项,不是“行”本身。如果 WHERE 条件没走索引,InnoDB 会扫描聚簇索引全部记录并加 next-key lock,效果等同于表级锁,高并发下直接卡死。
典型失效场景包括:对非索引字段查询、使用函数(如 WHERE DATE(created_at) = '2026-06-15')、隐式类型转换(如 user_id 是 INT,却传字符串 '123')。
- 验证是否走索引:
EXPLAIN SELECT ... FOR UPDATE,重点看type(应为const/ref/range)和key字段(非NULL) - 联合索引注意最左匹配:索引
(a,b),WHERE b = 1无法使用 - 字符集/排序规则不一致也会导致索引失效,比如字段是
utf8mb4_0900_as_cs,而参数是utf8mb4_general_ci
UPDATE 之前真要先 SELECT FOR UPDATE 吗?
多数情况下不需要,甚至有害。UPDATE 语句本身就会在执行前对匹配行加 X 锁,原子性强、网络开销小、锁持有时间短。
只有两种情况值得额外加锁:
- 业务逻辑依赖读出的旧值做判断(例如“余额 > 100 才扣款”,且判断逻辑不能塞进
UPDATE WHERE子句) - 多步操作需要中间状态不被干扰(如先查库存、再查优惠券、最后统一扣减)
能写成单条 UPDATE ... WHERE balance > 100 的,就别拆成两步;拆了就必须确保整个流程在同一个事务里完成。
锁等待、死锁和超时怎么应对?
SELECT ... FOR UPDATE 阻塞是常态,不是 bug。后到事务会等待,默认 50 秒(由 innodb_lock_wait_timeout 控制),超时抛 Lock wait timeout exceeded;若被 InnoDB 死锁检测器选中,则报 Deadlock found when trying to get lock。
- 避免死锁:所有涉及多行的业务,强制按主键升序加锁,例如
SELECT ... FOR UPDATE ORDER BY id ASC - 减少阻塞影响:MySQL 8.0.1+ 支持
NOWAIT(立刻报错)和SKIP LOCKED(跳过已锁行),适合任务队列分发等场景 - 定位堵点:
SELECT * FROM information_schema.INNODB_TRX查当前事务,再关联INNODB_LOCK_WAITS分析阻塞链
真正容易被忽略的是:锁范围受隔离级别直接影响——REPEATABLE READ 下默认加 next-key lock(行锁 + 间隙锁),哪怕只查一行也可能锁住相邻间隙;READ COMMITTED 下只锁命中的行,但需确认业务能否容忍幻读。


















