for update 对满足 where 条件且走索引的行加排他行锁;有主键/唯一索引时仅锁匹配行,普通索引会加临键锁,无索引则可能全表扫描并锁所有扫描行;必须在显式事务中使用,否则锁立即释放;乱序加锁易致死锁;skip locked 可避免争抢阻塞。

for update 在 MySQL 中到底锁什么
FOR UPDATE 不是锁整张表,也不是锁索引范围,而是对 当前 SELECT 扫描到的、满足 WHERE 条件的行(且有对应索引支持)加排他行锁(X 锁)。前提是事务隔离级别为 REPEATABLE READ 或 READ COMMITTED,且查询走的是**唯一索引或普通索引(非全表扫描)**。
如果 WHERE 条件不走索引,MySQL 会退化为锁所有扫描过的记录(甚至可能升级为间隙锁+临键锁组合),极端情况下等效于表级锁定,严重拖慢并发。
- 有主键或唯一索引:只锁匹配的那 1 行
- 有普通索引但非唯一:锁匹配的行 + 对应的间隙(防幻读)
- 没索引 or 索引失效(如
WHERE name LIKE '%abc'):可能锁全表扫描涉及的所有行,风险极高
典型误用:在非事务块里执行 for update
SELECT ... FOR UPDATE 必须运行在显式事务中,否则语句执行完就自动提交,锁立刻释放 —— 这会让“先查后更”的逻辑彻底失效。
START TRANSACTION; SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; -- 此时其他事务对 id=123 的 UPDATE/SELECT FOR UPDATE 会被阻塞 UPDATE accounts SET balance = balance - 100 WHERE id = 123; COMMIT; -- 锁在此刻才释放
常见错误写法:
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; -- 没 START TRANSACTION!锁秒放 UPDATE accounts SET balance = balance - 100 WHERE id = 123; -- 此时已无锁保护,超扣风险
死锁是怎么被 for update 招来的
两个事务按不同顺序对同一组行加 FOR UPDATE,就极易触发死锁。MySQL 检测到后会回滚其中一个事务,并报错:Deadlock found when trying to get lock; try restarting transaction。
- 事务 A:先
SELECT ... WHERE id = 100 FOR UPDATE,再SELECT ... WHERE id = 200 FOR UPDATE - 事务 B:先
SELECT ... WHERE id = 200 FOR UPDATE,再SELECT ... WHERE id = 100 FOR UPDATE
解决办法只有两个:
- 所有业务路径统一按相同顺序(如总是按
id ASC)获取行锁 - 捕获死锁异常,在应用层重试整个事务(注意幂等性)
替代方案:用 select for update skip locked 避免排队
当多个线程争抢一批任务(如订单处理队列),传统 FOR UPDATE 会让后到者一直等待,响应变慢。MySQL 8.0+ 支持 SKIP LOCKED,跳过已被锁的行,直接拿下一个可用行:
START TRANSACTION; SELECT * FROM tasks WHERE status = 'pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; -- 即使其他事务已锁住前几行,这条语句也不会阻塞,而是返回下一个未被锁的 pending 任务 UPDATE tasks SET status = 'processing' WHERE id = ?; COMMIT;
注意:SKIP LOCKED 仅适用于 READ COMMITTED 隔离级别下效果最稳定;REPEATABLE READ 下某些场景可能行为不一致,需实测验证。
真正难的不是写对这句 SQL,而是想清楚「哪些数据必须强一致性」和「哪些可以接受短暂不一致」——锁是代价,不是装饰。










