RPOPLPUSH仅原子移动任务,不解决卡死问题;需配合ZADD+ZRANGEBYSCORE实现超时回填、分片队列隔离优先级、业务层幂等校验task_id、监控List长度防积压。

RPOPLPUSH 是循环调度的核心,但不是万能的
它确实能原子地把任务从一个 List 挪到另一个 List,比如 RPOPLPUSH task:queue processing:queue,但这个操作本身不解决“任务卡死”问题。很多人误以为挪过去就等于开始处理了,其实只是进了“待确认”状态。消费者一崩溃,processing:queue 里的任务就永远滞留——Redis 不会主动清理,也不会重发。
必须配合以下措施:
-
EXPIRE processing:queue 300给整个队列设 TTL(5 分钟),这是最简兜底;但要注意:List 无法给单个元素设过期,所以超时后整队列清空,可能误删还没超时的任务 - 更稳妥的做法是消费者拿到任务后,立刻用
PEXPIRE给该元素续期——不行,List 不支持单元素 TTL,这条路走不通 - 实际可行方案:挪完后,用
ZADD processing:zset {unix_ms} {task_json}把带时间戳的副本写进 Sorted Set,再靠ZRANGEBYSCORE processing:zset -inf {cutoff}扫描并回填超时任务
BRPOP 多队列监听能实现优先级,但顺序不是绝对的
像 BRPOP task:queue:high task:queue:low 0 这种写法,看似高优队列永远先被消费,其实有陷阱:Redis 检查顺序是左到右,但若高优队列为空、低优队列有数据,它会直接返回低优队列的元素——这没问题;但如果两个队列同时有数据,Redis 仍按参数顺序返回左边第一个非空队列的值,这点是确定的。
但真正的问题在客户端:多个工作节点并发执行 BRPOP,没有全局锁,会出现“高优任务被低优节点抢走”的情况。这不是 Redis 的错,而是调度逻辑缺失。
建议做法:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用不同 key 做队列隔离,比如
task:queue:high:{shard},靠哈希分片降低竞争 - 对高优任务加前缀或字段标记,在消费后做二次校验,不满足条件则
LPUSH回原队列头部 - 避免用单一
BRPOP混合监听,改用两层轮询:BLPOP task:queue:high 0超时后才切到低优队列
任务重复执行不能靠队列机制防,必须业务层幂等
RPOPLPUSH 成功只代表移动成功,不代表任务执行成功。网络抖动、进程 OOM、下游超时都可能导致“挪了但没处理完”,消费者重启后又会重新拉取——这就是重复执行的根源。
唯一可靠的方式是在业务逻辑开头检查是否已成功:
- 每个任务体里必须含全局唯一
task_id(推荐ULID或雪花 ID,别用自增) - 执行前查
GET finished:{task_id}或HGET tasks:{task_id} status,命中就直接跳过 - 成功后立刻写
SET finished:{task_id} 1 EX 86400,TTL 设长一点,避免刚写完就过期 - 千万别用
RPOPLPUSH返回值做判断——它不带上下文,也不包含重试次数或时间戳
高并发下 List 性能够用,瓶颈在外部链路
RPOPLPUSH 是 O(1),BRPOP 也是常数级,Redis List 本身扛几万 QPS 没压力。真正拖慢系统的,往往是这些环节:
- 每次消费都要查 DB 或调第三方 API,没加缓存或批量聚合
- 超时扫描用
KEYS processing:*遍历,阻塞主线程——必须改用SCAN+ 游标分批 - 幂等检查用
HGETALL拉全量字段,而其实只需要 status 字段——应拆成单独的 Hash field 存储 - 失败重试没退避策略,一堆任务在
task:queue:retry里瞬间涌出,打垮下游
最容易被忽略的是:List 队列长度监控。不设 LLEN 告警,等积压到内存爆掉才感知到问题。上线前务必配好 redis-cli --stat 或 Prometheus exporter 抓 list_length 指标。

















