登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 多个消费者怎么用 SKIP LOCKED 领取任务

来源:17golang原创

时间:2026-09-06 08:38:43 184浏览 收藏

多个消费者共同读取 MySQL 任务表时,核心写法是把领取动作放进短事务:先用 SELECT ... FOR UPDATE SKIP LOCKED 锁定一条待处理任务,再在同一事务里把它改成 processing,最后提交。其他消费者遇到这条已锁记录会直接跳过,继续尝试下一条,而不是排队等待。

要点速览
  • SKIP LOCKED 适合队列式表,不适合要求完整一致视图的普通查询。
  • 领取和状态更新必须在同一个 InnoDB 事务中,提交后再执行耗时业务。
  • 没有任务、消费者崩溃、复制模式和长任务超时,都要有明确的恢复策略。

先把任务表设计成可并发领取

先准备一个状态清晰、能快速筛选的队列表。下面的字段足够演示领取路径,生产环境还可以增加重试次数、租约截止时间和错误摘要。

CREATE TABLE task_queue (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    payload JSON NOT NULL,
    status ENUM('pending', 'processing', 'done', 'failed') NOT NULL DEFAULT 'pending',
    claimed_by VARCHAR(64) NULL,
    claimed_at DATETIME NULL,
    PRIMARY KEY (id),
    KEY idx_queue_status_id (status, id) -- 让领取条件和排序共享索引
) ENGINE = InnoDB;

status, id 联合索引对应 WHERE status = 'pending' ORDER BY id。它不是为了保证绝对公平,而是让消费者有稳定的扫描顺序;被锁住的旧任务会被跳过,后面的任务可以先被领取。

字段或条件作用领取时的判断
status表达任务生命周期只选择 pending
id稳定排序和唯一定位领取后用主键更新
claimed_by记录消费者身份便于审计与排障
claimed_at记录租约或超时起点恢复卡住任务时使用
MySQL SKIP LOCKED 任务队列中的任务表字段、状态索引与消费者连接关系
图1:任务状态、联合索引和多个消费者连接共同构成可并发领取的队列表结构。

在短事务里用 FOR UPDATE SKIP LOCKED 抢一条

每个消费者都要使用自己的数据库连接,并显式开启事务。锁定读只有在事务未提交时才有意义;如果开启自动提交,锁可能在语句结束时就释放,领取和标记就无法形成一个原子动作。

START TRANSACTION;

-- 只找待处理且当前没有被其他消费者锁住的任务
SELECT id, payload
  FROM task_queue
 WHERE status = 'pending'
 ORDER BY id
 LIMIT 1
 FOR UPDATE SKIP LOCKED;

-- 应用拿到 id 后,在同一事务内完成状态转移
UPDATE task_queue
   SET status = 'processing',
       claimed_by = 'worker-a',
       claimed_at = NOW()
 WHERE id = 12345
   AND status = 'pending'; -- 防止状态已经变化时误更新

COMMIT; -- 提交后才释放领取锁

应用层要检查查询是否返回了行,以及 UPDATE 的影响行数。查询没有结果时说明当前没有可领取任务,可以短暂退避后重试;不要把它当成异常死循环。两个消费者同时执行时,已被第一个事务锁住的记录会从第二个结果集中消失,但第二个消费者仍可能读到更靠后的任务。

MySQL 官方说明了这个语义:SKIP LOCKED 不等待行锁,直接从结果集中移除被锁行;因此返回的是不一致视图,只适合队列一类允许跳过锁行的场景。它只对行级锁生效,使用语句复制时还要单独评估安全性。

把数据库领取和业务执行分开

不要在事务里调用远程 API、生成文件或执行几分钟的业务。正确边界是“短事务只负责认领,提交后再处理”:任务进入 processing 后,消费者可以在事务外完成工作,成功后更新为 done,失败则写入 failed 或增加重试次数。

进程在提交前崩溃,事务回滚,任务仍是 pending;进程在提交后、业务完成前崩溃,任务可能停留在 processing。后一种情况不能依赖锁自动解决,应结合 claimed_at 做租约超时恢复,并让业务操作具备幂等性。否则恢复消费者可能再次执行已经完成一半的外部动作。

MySQL 领取事务与事务外业务执行的边界,以及 processing、done、failed 状态关系
图2:短事务只覆盖锁定和状态转移,耗时处理位于事务外,并通过状态与租约支持恢复。

上线前检查这几项并发边界

  • 确认表使用 InnoDB,查询条件包含明确的 ORDER BYLIMIT,并检查 (status, id) 索引。
  • 确认每次领取都显式提交或回滚;连接归还连接池前不能留下未结束事务。
  • 为空队列设置退避,死锁或瞬时连接错误采用有限次数重试。
  • processing 任务保存领取者和时间,定时恢复超时租约。
  • 把外部副作用设计成幂等,并根据部署环境评估复制方式和锁等待监控。

常见问题

SKIP LOCKED 会保证任务严格按 id 顺序处理吗?

不会。它只提供“遇到已锁行就跳过”的并发行为,后面的任务可能先完成。需要严格顺序时,应改用单消费者、分区队列或业务层排序。

查询不到任务时应该回滚吗?

如果已经开启事务,建议回滚或提交后再退避,确保连接没有留下未结束事务。空结果本身不是数据库错误。

为什么任务状态已经是 processing,却仍然重复执行?

通常是消费者在提交领取后崩溃,恢复逻辑又重新拿走了超时任务。需要租约、尝试次数和幂等键一起控制,而不是只依赖状态字段。

可以用普通 SELECT 再 UPDATE 代替吗?

不建议。普通读取和更新之间存在竞争窗口,多个消费者可能读到同一行;要么使用锁定读,要么采用带条件的原子更新并设计清晰的重试结果。

因此,MySQL 多消费者队列的最小可靠组合是:InnoDB、短事务、FOR UPDATE SKIP LOCKED、同事务状态更新,以及事务外的幂等执行和超时恢复。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>