




事务隔离级别决定锁行为:READ COMMITTED下FOR UPDATE仅锁命中行,REPEATABLE READ默认加间隙锁防幻读但易死锁,SERIALIZABLE使所有SELECT加共享锁;INSERT…ON DUPLICATE KEY UPDATE依赖唯一索引记录锁,不加间隙锁;显式加锁需索引命中、避免耗时操作与顺序不一致;自增锁在高并发插入时成瓶颈,建议调优模式或改用无锁ID。
MySQL 的 SELECT ... FOR UPDATE 或 INSERT ... ON D 是否加锁、加什么锁,首先取决于当前事务的隔离级别。在 
READ COMMITTED 下,普通 SELECT 不加锁,但 FOR UPDATE 只锁定命中行;而在 REPEATABLE READ(InnoDB 默认)下,它还会隐式加间隙锁(Gap Lock),防止幻读——这既是保护,也是死锁温床。
WHERE status = 0 LIMIT 10),REPEATABLE READ 下可能锁住整个索引区间,阻塞并发插入SET TRANSACTION ISOLATION LEVEL READ COMMITTED,减少间隙锁范围SERIALIZABLE 会把所有普通 SELECT 都转成 SELECT ... LOCK IN SHARE MODE,几乎等于串行化,极少用当存在唯一键(UNIQUE 或 PRIMARY KEY)时,INSERT ... ON DUPLICATE KEY UPDATE 在内部由 MySQL 自动处理“先查后插/更”的竞争,无需手动加锁,且只在发生冲突时才触发更新逻辑。它底层依赖的是唯一索引上的记录锁(Record Lock),不会扩展到间隙。
INSERT INTO order_log (order_id, status, updated_at) VALUES (123, 'paid', NOW()) ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at = NOW();
order_id 有唯一索引,否则语句不生效或报错UPDATE 子句中引用了未在 VALUES() 中提供的列(如 updated_at = updated_at + 1),要注意是否真的需要该语义——多数场景应直接赋值 NOW()
ROW 格式记录,主从一致,但若用 MIXED 模式,某些函数(如 NOW())可能被转成 STATEMENT,引发主从时间不一致当你必须“读出再计算再更新”(比如扣库存:查余额 → 判断是否足够 → 扣减),就得用 SELECT ... FOR UPDATE。但它不是万能解药,容易成为性能瓶颈和死锁源头。
WHERE 条件上命中索引,否则会升级为表锁(尤其在 REPEATABLE READ 下)FOR UPDATE 语句访问行的顺序不一致(A 先锁 id=1 再锁 id=2,B 反过来),是典型死锁诱因;建议统一按主键升序加锁SELECT ... FOR UPDATE SKIP LOCKED(MySQL 8.0+)跳过已被锁的行,适合队列类消费场景InnoDB 的自增锁(auto-inc lock)在高并发 INSERT 时可能成为隐形瓶颈。默认 innodb_autoinc_lock_mode = 1(连续模式)下,简单插入(不含 INSERT ... SELECT)不锁表,但批量插入仍需获取表级自增锁。
INSERT INTO t SELECT ... FROM other_t,考虑调大 innodb_autoinc_lock_mode = 2(交错模式),但要求 binlog 格式为 ROW
AUTO_INCREMENT 值做业务逻辑(如“订单号=时间戳+自增ID”),因为 ID 分配不连续、不可预测真正难的不是写对一条 FOR UPDATE,而是理清哪条语句在什么索引路径下会锁住哪些索引项、是否包含间隙、会不会被其他事务的相同语句反向覆盖——这些只有看 INFORMATION_SCHEMA.INNODB_TRX 和 INNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(8.0+)才能确认。