当前位置:首页 > 文章列表 > 数据库 > 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推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    463次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    527次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    545次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    494次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    313次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码