zadd用时间戳作score易漏任务,因系统时钟回拨、服务时间不同步或客户端时间不准导致score小于当前时间;须统一用毫秒级整数、写入前校验score≥当前时间、由消费者本地生成时间戳。

为什么 zadd 时间戳当 score 容易漏任务
Redis ZSET 延时队列本质是靠 zadd 把任务塞进有序集合,score 用时间戳(秒级或毫秒级),再用 zrangebyscore 拉出已到时间的任务。但很多人直接用 time.time() 当 score,结果发现:刚加进去的任务立刻被消费了。
原因很简单:系统时钟可能回拨、不同服务时间不同步、或者你本地跑测试时手快多点了几次,导致写入的 score 小于当前服务器时间。ZSET 不校验逻辑合理性,只按数字大小排。
- 务必用
int(time.time() * 1000)统一毫秒级,避免浮点误差和精度丢失 - 写入前检查
score >= current_ms,不满足就 warn 或 fallback 到current_ms - 不要依赖客户端时间,所有时间戳由消费者所在服务生成(比如 Celery worker 本地取)
zpopbyscore 不存在?得用 zrangebyscore + zrem 组合
Redis 原生没有原子性地“取出并删除”指定 score 范围成员的命令。zpopbyscore 是 Redis 6.2+ 才加的,而且默认不带 WITHSCORES,老版本根本不能用。
生产环境大概率是 Redis 5.x 或 6.0,必须自己组合操作。但要注意并发:两个 worker 同时 zrangebyscore 拿到同一堆任务,都去 zrem,会重复执行。
立即学习“Python免费学习笔记(深入)”;
- 用 Lua 脚本封装
zrangebyscore+zrem,保证原子性(示例里 key 是delay_queue,score 上界是now_ms):
eval "local res = redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 10); if #res > 0 then redis.call('zrem', KEYS[1], unpack(res)); end; return res" 1 delay_queue 1717023456000- 别用
ZRANGEBYSCORE ... WITHSCORES然后手动解析数组——返回值是 [member1, score1, member2, score2],容易索引错位 - 限制每次最多取 10~20 条,避免单次操作太久阻塞 Redis
任务失败后怎么重试?别直接 zadd 回原队列
延时队列不是“失败就重来”的地方。如果一个任务执行失败,直接 zadd delay_queue now_ms+60000 task_data,看起来是 1 分钟后重试,但实际会破坏 ZSET 的时间序:新 score 可能比队列里其他 pending 任务还小,导致它插队执行。
更糟的是,如果失败是永久性的(比如参数错误、下游不可用),这个任务会不断重试、卡住整个消费节奏。
- 失败任务应转入独立的
retry_queue,score 设为指数退避值(如 60s → 300s → 900s) - 永远不要在重试逻辑里用
time.time(),要用上一次失败时刻 + delay,否则时钟漂移会导致重试时间不准 - 加个最大重试次数字段,存进 value 里(比如 JSON 字符串),超过就丢进死信队列
dlq:delay
Python 用 redis-py 连接时,socket_timeout 和 health_check_interval 必须设
延时队列对连接稳定性极其敏感。worker 长期空闲时,中间网络设备(NAT、负载均衡器)可能静默断开 TCP 连接,但 client 还以为连着,下次 zrangebyscore 就卡住或报 ConnectionError。
这不是代码逻辑问题,是连接管理没配好。
-
socket_timeout=5强制命令超时,避免 hang 死 -
health_check_interval=30让 client 每 30 秒发个PING探活,自动重连断开的连接 - 别用
ConnectionPool默认配置——它的max_connections是 2**31-1,看似无限,实则会撑爆文件描述符 - 示例初始化:
r = redis.Redis(connection_pool=redis.ConnectionPool(
host='localhost',
port=6379,
db=0,
socket_timeout=5,
health_check_interval=30,
max_connections=20
))Redis ZSET 延时队列真正难的不是怎么塞数据,而是怎么让时间戳可靠、怎么防并发误取、怎么把失败隔离清楚。这三个点里任意一个没压住,线上就会出现任务丢失、重复、堆积。尤其时间戳,最容易被当成“小问题”跳过,结果查三天才发现是时区或精度搞错了。










