SKIP_LOCKED 并非开箱即用的并发加速器,仅在索引覆盖、显式事务、合适隔离级别(READ COMMITTED)及正确SQL结构下生效;缺失任一条件仍会导致排队、重复或死锁。

SKIP_LOCKED 不是开箱即用的“并发加速器”,它只在特定结构+事务+索引组合下才真正生效;漏掉任一环节,照样排队、重复、死锁。
为什么加了 SKIP_LOCKED 还在排队?
根本原因不是语法写错了,而是查询没落到“可跳过的行集合”上:
- WHERE 条件没走索引,导致 MySQL 全表扫描,锁住大量无关行,
SKIP_LOCKED只能跳过“已被锁的行”,但无法跳过“本不该被扫到的行” - ORDER BY 字段无索引或存在大量重复值(比如都为
'pending'),优化器可能每次选中不同物理行,多个事务看似跳过了彼此,实则捞出同一条业务逻辑上不可用的记录 - 事务未显式开启:
AUTOCOMMIT=1下,SELECT ... FOR UPDATE SKIP_LOCKED执行完立刻释放锁,下一条UPDATE就是新事务,别人已插队 - 隔离级别为
READ UNCOMMITTED或SERIALIZABLE:前者不支持SKIP_LOCKED,后者会禁用该行为(报错ER_NOT_SUPPORTED_YET)
怎么写出真正不冲突的抢单 SQL?
必须把“查、锁、判、更”压进一个事务,并确保索引覆盖全部过滤和排序路径:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 建表时给关键字段加联合索引,例如:
CREATE INDEX idx_status_stock ON items (status, stock, updated_at)—— 顺序不能错,status在前才能高效过滤,updated_at在后才支撑ORDER BY updated_at ASC - 事务内第一句必须是带
SKIP_LOCKED的SELECT,且只查主键和必要字段,避免锁升级 - 拿到结果后立刻检查
id是否为空;若为空,说明无可用库存,直接COMMIT退出,不要重试或 sleep - 更新语句必须用主键精确匹配:
UPDATE items SET stock = stock - 1 WHERE id = ? AND stock > 0—— 后置校验防超卖,哪怕 SELECT 时 stock > 0,UPDATE 前也可能被别人改掉
哪些场景下 SKIP_LOCKED 会失效?
它不是万能锁逃逸方案,以下情况它帮不上忙:
- 热点单行更新:比如所有抢购都扣同一张优惠券的总库存(单行
coupon_id = 1),SKIP_LOCKED没意义——没别的行可跳,大家还是卡在这一行上等待 - 子查询中使用:
UPDATE orders SET status = 'paid' WHERE id IN (SELECT id FROM items FOR UPDATE SKIP LOCKED)会直接报错ER_NOT_SUPPORTED_YET - 用
READ COMMITTED隔离级别但没包住整条链路:SELECT 锁住的行在语句结束就释放,UPDATE 时锁已不在,应用层需自己保证原子性 - WHERE 中混用函数或类型隐式转换,导致索引失效,退化为全表扫描 + 大量无效加锁
比 SKIP_LOCKED 更容易被忽略的细节
很多人调通了 SQL 就以为万事大吉,但线上真正卡住的,往往是这些“非 SQL 层”问题:
- 连接池配置不合理:
maxActive=20却启动了 50 个消费者线程,大量线程卡在获取连接上,根本到不了数据库层 -
innodb_lock_wait_timeout设得太小(如 5 秒),导致本可等待成功的事务被强制回滚,反而抬高重试率 - 没监控
INFORMATION_SCHEMA.INNODB_TRX和INNODB_LOCK_WAITS,直到用户投诉才发现有长事务一直没提交,把整条队列堵死了 - 应用层没做幂等:同一个
id被多个线程 SELECT 到(因 ORDER BY 字段重复 + 无索引),又没校验业务状态,导致重复发券










