开放问答
SELECT FOR UPDATE 走二级索引却把不相干的行也锁住,如何量化并缩小锁范围
该问题的正文、回答与评论。
提问者 陈砚舟 · 状态 open
MySQLInnoDB锁SELECT FOR UPDATEgap lock并发
背景:MySQL 8.0.36,InnoDB,REPEATABLE READ。一张 3200 万行的工单表 tickets,主键 id(自增),二级索引 idx_tenant_status (tenant_id, status, create_time)。业务有一个"领取工单"操作:
SELECT id FROM tickets WHERE tenant_id = ? AND status = 'PENDING' ORDER BY create_time ASC LIMIT 1 FOR UPDATE;
拿到的 id 之后再 UPDATE tickets SET status='PROCESSING', assignee=? WHERE id=?。
现象:
- 白天高峰期,多个 worker 并发领取同一个 tenant 的工单时,经常有 worker 卡 15–30s 才拿到锁,期间
SHOW ENGINE INNODB STATUS的 LATEST DETECTED DEADLOCK 里能看到 gap lock 等待,但两条语句锁的 id 并不重叠; - 更诡异的是,另一个只读服务做
SELECT COUNT(*) FROM tickets WHERE tenant_id=?也被短暂阻塞(3–5s),理论上只读 RR 快照读不该被行锁挡住; performance_schema.data_locks里能看到大量LOCK_TYPE: GAP记录,range 跨度从几千到十几万行不等,明显大于"LIMIT 1 应该锁 1 行"的直觉。
已尝试:
- 把
ORDER BY create_time去掉,让查询只锁"第一个命中的 PENDING 行",死锁少了但业务方不接受乱序领取; - 改
FOR UPDATE SKIP LOCKED,并发冲突消失了,但业务要求"必须领到最老的一单",SKIP LOCKED 会跳过被锁的行去领下一单,语义不符; - 查过 innodb_lock_wait_timeout=50s,调小没用,因为问题不是"等太久"而是"不该等";
- 怀疑过是 idx_tenant_status 上有大量 PENDING 行导致 range 扫描 + gap lock 连锁,但没找到量化工具确认"一条 SELECT FOR UPDATE 到底锁了多少行/多少 gap"。
想请教:
- 有没有一种低侵入的方法能精确量化一条 SELECT FOR UPDATE 实际持有的锁范围? 我想到 performance_schema.data_locks 按 trx_id 过滤,但锁是动态的,抓到的快照不一定覆盖完整生命周期。有没有更好的"锁足迹"观测手段?
- RR 下二级索引 range 扫描的 gap lock 到底锁到哪? 是锁到"第一个不满足条件的行"还是锁到"索引 B+ 树叶子节点边界"?如果 PENDING 行后面紧跟大量 COMPLETED 行,gap 会不会一直延伸到下一个 tenant 的边界?想确认一下 InnoDB 在这种场景下 gap lock 的精确语义。
- 有没有既保证"领取最老的一单"又避免大范围 gap lock 的写法? 我想到两个方向:(a) 先不加锁
SELECT ... ORDER BY create_time LIMIT 1,再UPDATE ... WHERE id=? AND status='PENDING'用条件更新做 CAS,失败重试;(b) 在应用层用"按 tenant 分桶 + 桶内队列"把并发度降下来。想听听大家的实际取舍,哪种在生产上更稳?
评论
暂无评论。