FOR UPDATE 必须在事务中才生效
单独执行
SELECT ... FOR UPDATE不会加锁,MySQL 会直接报错或静默忽略(取决于 SQL 模式),真正起作用的前提是当前连接已开启事务且未提交。常见错误是写完
SELECT ... FOR UPDATE就立刻执行
UPDATE,但没用
BEGIN或
START TRANSACTION包裹,结果锁根本没拿到,后续更新发生并发覆盖。
实操建议:
务必显式开启事务:BEGIN→
SELECT ... FOR UPDATE→ 业务逻辑 →
COMMIT或
ROLLBACK避免在事务里做耗时操作(如远程调用、大循环),否则锁持有时间过长,阻塞其他事务 确认隔离级别:默认
REPEATABLE READ下,
FOR UPDATE会对扫描到的索引记录加 next-key lock;若降为
READ COMMITTED,则只加 record lock(不锁间隙)
行锁是否生效,取决于 WHERE 条件是否命中索引
FOR UPDATE看似“锁行”,实际锁的是索引项。如果
WHERE条件没走索引(例如对非索引字段查询、或用了函数/隐式类型转换),MySQL 会退化为锁全表(更准确地说:锁所有聚簇索引记录,即整张表的主键行),并发性能骤降。
验证方法:
执行前先看执行计划:EXPLAIN SELECT ... FOR UPDATE,确认
key列非
NULL,且
rows尽量小 检查是否因字符集/排序规则不一致导致索引失效,例如
WHERE name = 'abc',但
name是
utf8mb4_0900_as_cs而参数是
utf8mb4_general_ci联合索引要注意最左匹配:若索引是
(a, b),
WHERE b = 1无法使用该索引,自然也不触发行锁
阻塞不是 bug,而是锁等待超时触发的 ERROR 1205 或 ERROR 1213
两个事务同时尝试
FOR UPDATE同一行,后到者不会立即失败,而是进入锁等待状态,默认等待 50 秒(由
innodb_lock_wait_timeout控制),超时后抛出
ERROR 1205 (HY000): Deadlock found when trying to get lock(死锁)或
ERROR 1213 (40001): Deadlock found when trying to get lock(注意:1205 和 1213 实际都表示锁等待失败,具体报哪个取决于是否被检测为死锁)。
关键点:
死锁检测是自动的,但代价不低;高并发下频繁死锁说明业务逻辑存在竞争热点,比如多个服务争抢同一账户余额 不要依赖超时来“兜底”,应在应用层设计重试机制,并限制最大重试次数(避免雪崩) 用SHOW ENGINE INNODB STATUS\G查看最近死锁详情,重点关注
TRANSACTION和
WAITING FOR THIS LOCK TO BE GRANTED部分
UPDATE 语句本身也会隐式加锁,不一定非要写 FOR UPDATE
很多人以为只有显式
SELECT ... FOR UPDATE才加锁,其实普通
UPDATE在执行时,MySQL 会对将要修改的行自动加 X 锁(排他锁),效果等同于先
SELECT ... FOR UPDATE再
UPDATE。区别在于:前者可提前暴露锁冲突(在 SELECT 阶段就阻塞),后者把冲突推迟到 UPDATE 阶段。
适用场景选择:
需要基于当前值做判断再决定是否更新(如“余额 > 100 才扣款”),必须用SELECT ... FOR UPDATE,否则可能读到旧值导致逻辑错误 只是简单 SET 字段(如
UPDATE t SET status = 'done' WHERE id = 123),直接
UPDATE更简洁,也少一次网络往返 注意:
UPDATE若影响 0 行,锁也会释放;而
SELECT ... FOR UPDATE即使查不到记录,也可能锁住间隙(gap lock),防止幻读 锁的粒度、行为和触发条件高度依赖索引结构与事务上下文,脱离具体表结构和查询条件谈“FOR UPDATE 是否加行锁”,大概率会误判。
