FOR UPDATE SKIP LOCKED 能解决任务重复消费,因为它仅返回当前未被任何事务锁定的行并立即加锁,确保并发查询自动跳过已被抢占的行。需配合显式事务、LIMIT 1、索引WHERE条件使用,且不解决业务状态竞态,须辅以幂等设计。

SKIP LOCKED 为什么能解决任务重复消费
任务队列里多个工作进程同时 SELECT 待处理任务时,若不加控制,很容易查到同一行并各自 UPDATE,造成重复执行。传统做法是加应用层分布式锁,但引入额外组件、增加延迟和故障点。MySQL 8.0 的 FOR UPDATE SKIP LOCKED 让数据库自己做“排他抢占”:它只返回**当前未被任何事务锁定**的行,并立即对其加锁,其他并发查询自动跳过这些行——不是靠运气避开,而是由引擎保证语义正确。
必须配合显式事务和 LIMIT 1 使用
单独写 SELECT * FROM tasks WHERE status = 1 FOR UPDATE SKIP LOCKED 是危险的:如果没有 LIMIT,可能一次锁住成百上千行;如果没有 START TRANSACTION,语句执行完就自动提交,锁立刻释放,根本起不到“抢占后独占处理”的作用。
- 必须用
START TRANSACTION开启事务,后续UPDATE和COMMIT才能绑定同一行锁 - 必须带
LIMIT 1(或更小值),确保每次只争抢一个任务,避免锁竞争放大 - WHERE 条件要走索引(如
status字段上有索引),否则会退化为表扫描+全表加锁,SKIP LOCKED失效
别把 SKIP LOCKED 当成万能药:它不解决状态竞态
SKIP LOCKED 只跳过**已被其他事务加了行锁**的记录,但它不感知业务状态是否已变更。典型坑点:
- 事务 A 查到
id=100, status=1并加锁,但还没来得及UPDATE status=2就崩溃了 → 锁自动释放,但状态仍是 1 → 事务 B 可能再次查到它 - 事务 A 成功更新
status=2但没提交,事务 B 的SELECT ... SKIP LOCKED仍会跳过该行(因为锁还在),这是对的;但如果事务 A 回滚,该行又变回可被抢的状态,也合理 - 真正要防的是“已逻辑完成但 DB 状态未更新”,这得靠幂等设计或状态机校验,不能只依赖
SKIP LOCKED
Python + PyMySQL 示例中容易漏掉的关键点
很多示例代码只贴了 SQL,但实际集成时这几个细节常被忽略:
- 连接不能设
autocommit=True,否则FOR UPDATE SKIP LOCKED执行完就释放锁,后续UPDATE操作不在同一个事务里 - 捕获
EmptySet或无结果返回时,要明确区分“没任务了”和“有任务但全被锁了”,后者建议短暂休眠后重试,避免空轮询打爆 CPU - 如果任务表有软删除字段(如
is_deleted),WHERE 条件必须包含AND is_deleted = 0,且该字段也要加入索引覆盖,否则SKIP LOCKED可能跳过不该跳过的行
最易被忽略的是:SKIP LOCKED 不改变隔离级别行为,它只是在加锁阶段跳过已锁行;如果你用的是可重复读(RR),那后续 SELECT 仍看到事务开启时的快照,别误以为“锁住了就能读到最新状态”。


















