redis原生pub/sub不支持公平调度,因其subscribe是广播模型,每条消息推送给所有订阅者,无轮询、权重或消费确认机制,易导致重复消费、任务无法接管及并发失控;须用list+lua模拟:lpush入队保证有序,brpop+setnx加租约锁实现公平取任务,租约过期后自动重试。

Redis原生Pub/Sub不支持公平调度,必须用List + Lua组合模拟才能实现消费者负载均衡
为什么SUBSCRIBE不能做公平分发
Redis的SUBSCRIBE是广播模型:每条PUBLISH消息会无差别推送给所有在线订阅者。没有内置的“轮询”“权重”或“消费确认”机制,也无法感知下游消费者是否卡住、超时或崩溃。
常见错误现象包括:
- 多个Worker同时收到同一条消息,各自处理一遍(重复消费)
- 某个Worker卡死,其他Worker无法接管未完成任务
- 无法限制并发数,容易压垮下游服务
所以直接用SUBSCRIBE做任务分发,本质是“广播通知”,不是“任务队列”。要公平调度,得换思路。
用LPUSH + BRPOPLP + Lua保证原子性入队
核心是把“发布”动作从PUBLISH换成LPUSH到一个List,再用Lua封装入队逻辑,避免竞态。
示例脚本(保存为enqueue.lua):
local queue_key = KEYS[1]
local msg = ARGV[1]
local ts = ARGV[2] -- 可选时间戳用于排序
return redis.call("LPUSH", queue_key, ts .. "|" .. msg)
调用方式:
redis-cli --eval enqueue.lua mytaskqueue , "task:abc" "1748732400"
这样做的好处:
-
LPUSH天然有序,新任务总在队首 - Lua执行期间Redis单线程,不会被其他客户端打断
- 可扩展加入去重逻辑(如先
EXISTS查任务ID)
BRPOPLP + SETNX实现带租约的公平取任务
每个Worker不能直接BRPOP,否则还是可能抢到同一任务。要用Lua+SETNX加临时锁:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
示例脚本(claim_task.lua):
local queue_key = KEYS[1]
local lock_key = "lock:" .. queue_key
local worker_id = ARGV[1]
local lease_sec = tonumber(ARGV[2]) or 30
<p>-- 尝试弹出一个任务
local task = redis.call("RPOP", queue_key)
if not task then return nil end</p><p>-- 加租约锁,过期自动释放
local locked = redis.call("SETNX", lock_key, worker_id)
if locked == 1 then
redis.call("EXPIRE", lock_key, lease_sec)
return task
else
-- 加锁失败,把任务放回去(注意:这里用LPUSH保持原序)
redis.call("LPUSH", queue_key, task)
return nil
end</p>
关键点:
-
RPOP+SETNX组合确保只有一个Worker拿到该任务 -
EXPIRE防止Worker崩溃后任务永久丢失 - 失败时
LPUSH回原List,不破坏顺序
注意:BRPOPLP不存在——别被拼写误导,实际就是BRPOP或RPOP,只是加了Lua兜底。
消费确认与重试必须由业务层控制
Redis不做ACK,所以“处理完成”必须显式标记。推荐用两个List:
-
mytaskqueue:待处理队列 -
mytaskdone:成功完成的任务ID(可选)
Worker处理完后,执行:
redis-cli LREM mytaskqueue 0 "task:abc"
或者更安全地用Lua检查租约是否仍在:
if redis.call("GET", "lock:mytaskqueue") == ARGV[1] then
return redis.call("LREM", KEYS[1], 0, ARGV[2])
else
return 0
end
没ACK的任务,等租约过期后会被其他Worker重新RPOP尝试——这就是公平调度的实质:靠租约+重试,而不是靠Redis内置机制。
最容易被忽略的是租约时间设置:太短会导致正常任务被误重试,太长会导致故障恢复延迟。建议设为预估最大处理耗时的2–3倍,并配合监控告警。










