当前位置:首页 > 文章列表 > 数据库 > Redis > Redis ZSet 延迟任务如何处理:ZRANGEBYSCORE 取出与 ZREM 抢占的竞态

Redis ZSet 延迟任务如何处理:ZRANGEBYSCORE 取出与 ZREM 抢占的竞态

来源:17golang原创 2026-08-29 16:27:59 0浏览 收藏

订单预约时间到了以后,两个 worker 都从 Redis 的延迟集合里拿到同一个任务,这类重复执行通常不是 ZSet 排序失效,而是“先读后删”把抢占拆成了两个时间点。把到期筛选和删除放进同一个 Lua 脚本,才能让一个 job_id 只被一个 worker 取走;脚本返回空结果时,另一个 worker 应该等待下一轮扫描。

Redis ZSet 只负责按 score 找到到期任务,真正的并发安全点是让 ZRANGEBYSCORE 与 ZREM 在同一次原子操作里完成。

要点速览
  • ZRANGEBYSCORE 负责找出 score 不大于当前时间的候选任务。
  • 单独先读后删会让 worker-a 与 worker-b 同时看到同一个 job_id。
  • Lua 脚本把 ZRANGEBYSCORE、ZREM 和 job_id 返回串成一次抢占。
  • 任务业务执行失败仍要靠重试状态、补偿队列或幂等键处理。

先把延迟任务拆成“到期”和“已抢占”

可以把 delay:jobs 设计成一个 Redis ZSet:member 是 job_id,score 是 Unix 时间戳。写入任务时用 ZADD delay:jobs 1787991500 order-731,worker 扫描时只关心 score 小于等于当前时间的成员。

这里有两个容易混在一起的动作。ZRANGEBYSCORE 只是读取候选,ZREM 才是把候选从待处理集合中移走。移走成功意味着当前 worker 获得了处理资格,不等于业务动作已经成功。

ZRANGEBYSCORE delay:jobs -inf 1787991600 LIMIT 0 1
ZREM delay:jobs order-731

Redis 官方文档说明,ZRANGEBYSCORE 按 score 范围返回成员,ZREM 则删除指定 member。两条命令分开发送时,中间天然存在一段空隙。

两个 worker 为什么会同时拿到同一个 job_id

线上最难发现的情况是:worker-a 读完候选后还没来得及 ZREM,worker-b 已经完成同样的读取。两边拿到的都是 job_id,随后各自执行订单通知、库存刷新或回调,重复副作用就发生了。

下面这段伪代码故意保留竞态,适合在代码评审中定位问题:

items := redis.ZRangeByScore("delay:jobs", "-inf", now, 0, 1)
if len(items) == 0 {
    return nil
}
jobID := items[0]
redis.ZRem("delay:jobs", jobID)
return jobID

即使 ZREM 最终只有一个调用返回 1,也已经晚了:两个 worker 都可能在删除前拿到了同一个 job_id。删除结果只能证明谁删掉了集合成员,不能追回已经开始的业务动作。

Redis ZSet 延迟任务中 worker-a 与 worker-b 同时读取 ZRANGEBYSCORE 后竞争 ZREM 的并发路径

用 Lua 把筛选和删除收成一个抢占点

抢占脚本只做三件事:从 delay:jobs 找一个到期 member,把这个 member 从集合中删除,再把 job_id 返回给调用方。Redis 执行 Lua 脚本期间不会插入另一个客户端的命令,因此两个 worker 不会在这三个动作之间交叉。

local items = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 1)
if #items == 0 then
  return false
end
local job_id = items[1]
local removed = redis.call('ZREM', KEYS[1], job_id)
if removed == 1 then
  return job_id
end
return false

调用方把当前时间作为 ARGV[1],把 delay:jobs 作为唯一 key 传入。第一个 worker 得到 job_id 后开始业务处理;第二个 worker 再运行脚本时,通常只会得到空结果。

Redis Lua 将 ZRANGEBYSCORE、ZREM 与 job_id 返回合并为一次原子抢占

别把“从集合移走”误当成业务成功

原子抢占解决的是重复领取,不是任务可靠完成。worker 在拿到 job_id 后宕机,任务已经从 delay:jobs 消失;如果没有状态记录,它就不会自动回来。

更稳妥的做法是把状态放在独立的哈希或数据库中:脚本成功返回后立即写入处理中记录,完成后改为成功;超时扫描处理中记录,再把仍可重试的 job_id 放回 ZSet。业务接口还应使用幂等键,避免“重试成功但第一次请求其实已落库”的反转情况。

阶段Redis 证据业务含义
待处理delay:jobs 中存在 job_id等待到期
已抢占ZREM 返回 1一个 worker 获得处理资格
已完成状态记录为 success业务动作已确认
待重试job_id 重新进入 ZSet补偿流程接管

高并发下的边界:批量、时钟和脚本长度

每次只抢一个任务最容易解释,但吞吐可能不够。批量抢占时可以让脚本取前 N 个到期 member,再逐个 ZREM,并返回成功删除的列表;N 不宜无限增大,否则一次脚本占用 Redis 主线程的时间会变长。

score 与 worker 当前时间必须使用同一时间基准。若生产机时钟漂移,某些任务会提前或延后进入候选范围。扫描间隔也不是精确触发器,延迟任务系统应该接受少量调度误差,真正的截止时间仍由业务处理逻辑复核。

上线前用四个问题检查抢占设计

  • 是否存在把 ZRANGEBYSCORE 和 ZREM 分成两次客户端请求的代码?
  • Lua 返回空结果时,worker 是否会在下一轮继续扫描而不是把它记成失败?
  • 任务从 ZSet 移走后,是否有处理中状态和超时补偿?
  • 业务副作用是否以 job_id 做幂等,能够承受重复回调?

相关问题

ZRANGEBYSCORE 能直接保证任务只执行一次吗?

不能。它只是按 score 读取成员;只读命令不负责抢占,必须配合原子删除或其他领取协议。

ZREM 返回 0 说明任务失败了吗?

不一定。它更可能表示 member 已被另一个 worker 删除,调用方应把它视为未获得处理资格,并交给下一轮扫描或状态查询。

为什么不只依赖 Redis 的过期键?

过期键适合清理和通知触发,不适合表达带 score 排序的任务领取、处理中状态和补偿关系;延迟任务通常需要单独的状态模型。

把判断留在抢占边界上

这套设计的关键不在于把所有逻辑塞进 Lua,而是只把“找一个到期任务并移走它”放进原子边界。业务处理、幂等、失败补偿仍然留在 worker 和状态存储中,各自的职责清楚,问题也更容易复查。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
MySQL 隔离级别下快照读为什么看不到刚提交数据:一致性读与当前读MySQL 隔离级别下快照读为什么看不到刚提交数据:一致性读与当前读
上一篇
MySQL 隔离级别下快照读为什么看不到刚提交数据:一致性读与当前读
Go bytes.Clone 如何隔离可变字节切片:拷贝边界与 nil 结果验证
下一篇
Go bytes.Clone 如何隔离可变字节切片:拷贝边界与 nil 结果验证
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    5425次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4911次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4834次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    5097次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    5056次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码