RR隔离级别下锁冲突高主因是间隙锁和临键锁不及时释放且范围失控;应将SELECT FOR UPDATE移至事务末尾、确保UPDATE走索引、用主键分批更新、空表INSERT用ON DUPLICATE KEY避免死锁。

RR(REPEATABLE READ)隔离级别下锁冲突和事务等待耗时高,不是因为“锁太多”,而是因为间隙锁(Gap Lock)和临键锁(Next-Key Lock)在事务提交前不释放,且锁范围容易失控。不降级隔离级别也能显著缓解——关键在控制加锁时机、缩小锁粒度、堵住隐式锁扩大路径。
为什么SELECT FOR UPDATE要放在事务最后一步
很多人把SELECT FOR UPDATE写在事务开头,以为“先锁住再干活”更安全。实际它让锁提前 3~5 秒持有时长:中间若调用 RPC、解析 JSON、校验参数,锁就空等在那里,后面所有想改同一行的事务全卡住。
- 正确做法:先做所有非 DB 操作(查缓存、验权限、算金额),只在真正要更新前一刻才执行
SELECT FOR UPDATE或直接UPDATE ... WHERE ... AND status = 'pending' - 效果:锁持有时间从“整个业务逻辑耗时”压缩到“SQL 执行+网络往返”,通常压到 10ms 内
- 注意:
SELECT FOR UPDATE必须跟在BEGIN之后、COMMIT之前,否则会报错SAVEPOINT does not exist
UPDATE语句的WHERE条件必须命中索引,且不能有函数包裹
没索引的WHERE会让 InnoDB 全表扫描并给每行加记录锁;即使有索引,用了函数也会让优化器放弃走索引,结果一样是锁全表。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 危险写法:
UPDATE orders SET status = 'done' WHERE DATE(created_at) = '2026-09-28'→created_at字段索引失效,触发全表扫描加锁 - 安全写法:
UPDATE orders SET status = 'done' WHERE created_at >= '2026-09-28 00:00:00' AND created_at - 检查方法:对语句执行
EXPLAIN,确认type是range或ref,且key列显示实际使用的索引名
批量更新必须按主键分段 + 即时提交
一条UPDATE t SET x = 1 WHERE created_at > '2026-09-01'可能锁住几千行,且这些锁要等到整个事务结束才释放。并发稍高,后续请求全排队等这一个事务。
- 拆分方式:用主键范围切片,例如
WHERE id BETWEEN 10000 AND 10999 AND created_at > '2026-09-01',每次最多更新 1000 行 - 必须每批执行后立刻
COMMIT,不能攒着一起提交——否则锁还是集中释放 - 补充技巧:如果表有自增主键且数据写入时间与 ID 高度相关,可直接用
id > ?代替时间条件,避免因时间字段无索引或索引选择率差导致锁扩大
空表或低数据量表插入死锁,本质是间隙锁争抢
RR 级别下,空表的主键间隙是(−∞, +∞),所有并发INSERT都在抢同一个 Insert Intention Lock,InnoDB 检测到循环等待后随机回滚一个事务,报错Deadlock found when trying to get lock。
- 首选解法:
INSERT INTO t (a, b) VALUES (?, ?) ON DUPLICATE KEY UPDATE b = VALUES(b),前提是a或b上有唯一索引(如业务单号) - 次选解法:会话级降为
READ COMMITTED,执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,此级别下间隙锁禁用,INSERT 只锁实际插入行 - 慎用解法:
REPLACE INTO或INSERT IGNORE,它们会先删后插或静默跳过,可能引发主键重用或 binlog 日志膨胀
最易被忽略的一点:RR 级别下,**锁永远落在索引上,而不是数据行上**。没有索引、索引未命中、用了非唯一二级索引,都会让锁范围指数级扩大——你以为只锁了一行,实际锁了整段间隙甚至全表。每次加锁前,先看EXPLAIN,再看SHOW ENGINE INNODB STATUS\G里的locks段,比调优参数实在得多。

















