事务隔离级别直接影响锁行为
MySQL 的
SELECT ... FOR UPDATE或
INSERT ... ON DUPLICATE KEY UPDATE是否加锁、加什么锁,首先取决于当前事务的隔离级别。在
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,几乎等于串行化,极少用
INSERT … ON DUPLICATE KEY UPDATE 是最轻量的写冲突处理
当存在唯一键(
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()该语句在 binlog 中以
ROW格式记录,主从一致,但若用
MIXED模式,某些函数(如
NOW())可能被转成
STATEMENT,引发主从时间不一致
显式加锁要慎用 SELECT … FOR UPDATE
当你必须“读出再计算再更新”(比如扣库存:查余额 → 判断是否足够 → 扣减),就得用
SELECT ... FOR UPDATE。但它不是万能解药,容易成为性能瓶颈和死锁源头。 务必在
WHERE条件上命中索引,否则会升级为表锁(尤其在
REPEATABLE READ下) 避免在事务里做耗时操作(如调用外部 HTTP、复杂计算),否则锁持有时间过长 多个
FOR UPDATE语句访问行的顺序不一致(A 先锁 id=1 再锁 id=2,B 反过来),是典型死锁诱因;建议统一按主键升序加锁 可以用
SELECT ... FOR UPDATE SKIP LOCKED(MySQL 8.0+)跳过已被锁的行,适合队列类消费场景
自增主键与 insert 并发的锁表现
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 分配不连续、不可预测 对于超高并发写入,可考虑分库分表、或改用雪花 ID 等无锁生成策略,把写压力从单表主键分配上卸下来
真正难的不是写对一条
FOR UPDATE,而是理清哪条语句在什么索引路径下会锁住哪些索引项、是否包含间隙、会不会被其他事务的相同语句反向覆盖——这些只有看
INFORMATION_SCHEMA.INNODB_TRX和
INNODB_LOCKS(MySQL 5.7)或
performance_schema.data_locks(8.0+)才能确认。
