mysql如何使用for update加锁_mysql行锁与阻塞

2次阅读

for UPDATE 必须在事务中生效,单独执行会报错或忽略;需BEGIN开启事务,确保WHERE命中索引以避免锁表,UPDATE本身也隐式加X锁。

mysql如何使用for update加锁_mysql行锁与阻塞

FOR UPDATE 必须在事务中才生效

单独执行 select ... FOR UPDATE 不会加锁,mysql 会直接报错或静默忽略(取决于 SQL 模式),真正起作用的前提是当前连接已开启事务且未提交。常见错误是写完 SELECT ... FOR UPDATE 就立刻执行 UPDATE,但没用 BEGINSTART TRANSACTION 包裹,结果锁根本没拿到,后续更新发生并发覆盖。

实操建议:

  • 务必显式开启事务:BEGINSELECT ... FOR UPDATE → 业务逻辑 → COMMITROLLBACK
  • 避免在事务里做耗时操作(如远程调用、大循环),否则锁持有时间过长,阻塞其他事务
  • 确认隔离级别:默认 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',但 nameutf8mb4_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 STATUSG 查看最近死锁详情,重点关注 TRANSACTIONWAITING FOR this LOCK TO BE GRANTED 部分

UPDATE 语句本身也会隐式加锁,不一定非要写 FOR UPDATE

很多人以为只有显式 SELECT ... FOR UPDATE 才加锁,其实普通 UPDATE 在执行时,MySQL 会对将要修改的行自动加 X 锁(排他锁),效果等同于先 SELECT ... FOR UPDATEUPDATE。区别在于:前者可提前暴露锁冲突(在 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 是否加行锁”,大概率会误判。

text=ZqhQzanResources