update默认加next-key锁属悲观锁;避开路径有三:①用insert...on duplicate key update替代(需唯一索引);②用update+version字段实现乐观锁;③批量更新拆为单条主键更新或走临时表join。

UPDATE 语句默认就带悲观锁,想避开得主动绕开
MySQL 的 UPDATE 在可重复读(RR)隔离级别下,默认会对匹配的行加 next-key 锁(行锁 + 间隙锁),本质就是悲观锁。这不是“有没有锁”的问题,而是“要不要让它锁住你不想锁的东西”。真正能避开悲观锁的路径,是让数据库不走索引扫描+加锁的老路,或者干脆不依赖行级锁机制。
用 INSERT ... ON DUPLICATE KEY UPDATE 替代条件 UPDATE
当你的更新逻辑本质是“存在则改,不存在则跳过或插入”,且表有唯一约束(如 UNIQUE 或 PRIMARY KEY),这个语法比 SELECT + UPDATE 更安全、更轻量——它只在冲突时才加锁,且锁粒度仅限于冲突的那一行(甚至可能只是意向锁),不会扫描全表或锁住范围。
- 必须确保
ON DUPLICATE KEY UPDATE的判定依据字段上有唯一索引,否则会退化为普通 INSERT 并报错 - 不能用于“WHERE 字段 LIKE 'xxx%'" 这类非精确匹配场景
- 注意:如果
UPDATE子句里引用了要插入的值(如val = VALUES(val)),VALUES()函数只在 INSERT 分支有效,别误写成val = val + 1—— 那会变成原地自增,仍可能触发锁
INSERT INTO user_balance (user_id, balance) VALUES (123, 100) ON DUPLICATE KEY UPDATE balance = balance + VALUES(balance);
用 UPDATE ... WHERE ... AND version = ? 实现乐观锁
这不是“避开锁”,而是把锁从数据库移到应用层逻辑里:靠版本号(version)或时间戳字段做并发控制。每次更新都校验当前版本是否未变,失败就重试。它不阻止并发读,也不阻塞其他 UPDATE,只在提交时检查冲突。
- 必须给表加一个
version INT DEFAULT 0(或updated_at TIMESTAMP)字段,并在每次 UPDATE 时显式带上校验条件 - 应用层需捕获
UPDATE返回的affected_rows;若为 0,说明版本已变,需重新 SELECT 再尝试 - 避免在 WHERE 中混用非索引字段,否则 MySQL 仍可能扫全表加锁——
version字段本身必须有索引支撑快速定位
UPDATE orders SET status = 'shipped', version = version + 1 WHERE id = 456 AND version = 2;
批量更新慎用 UPDATE ... IN (...),优先拆成主键等值更新
IN 列表如果参数多、无索引、或底层优化器选错执行计划,MySQL 可能放弃使用索引,转而走全表扫描并给所有扫描行加锁——这比单条 UPDATE 更危险。高并发下尤其容易引发锁等待雪崩。
- 确保
IN中的字段是主键或有覆盖索引,且列表长度可控(一般不超过几百) - 更稳妥的做法:把批量 ID 拆成多个单条
UPDATE ... WHERE id = ?,用连接池复用连接,反而更稳定 - 如果真要批量,考虑先写入临时表,再用
JOIN更新,但要注意临时表也要加索引,否则 JOIN 过程仍可能锁表
真正难的不是写出不锁的 SQL,而是厘清业务里哪些操作“必须强一致”,哪些可以接受短暂不一致或重试。比如库存扣减用乐观锁+重试,用户资料修改用唯一键冲突保护,日志类计数直接上 INSERT ... ON DUPLICATE KEY UPDATE——选型依据永远是业务语义,不是语法炫技。










