卢阿
我做了一些类似于这里建议的东西,但优化了睡眠持续时间以更精确。如果您在延迟的任务队列中插入很少,则此解决方案很好。以下是我使用 Lua 脚本的方法:
local laterChannel = KEYS[1]
local nowChannel = KEYS[2]
local currentTime = tonumber(KEYS[3])
local first = redis.call("zrange", laterChannel, 0, 0, "WITHSCORES")
if (#first ~= 2)
then
return "2147483647"
end
local execTime = tonumber(first[2])
local event = first[1]
if (currentTime >= execTime)
then
redis.call("zrem", laterChannel, event)
redis.call("rpush", nowChannel, event)
return "0"
else
return tostring(execTime - currentTime)
end
它使用两个“通道”。laterChannel
是一个ZSET
并且nowChannel
是一个LIST
。每当需要执行任务时,事件就会从 移动ZSET
到LIST
。Lua 脚本响应调度程序在下一次轮询之前应该休眠多少 MS。如果ZSET
是空的,就永远沉睡。如果是时候执行某事,请不要休眠(即立即再次轮询)。否则,休眠直到执行下一个任务。
那么如果在调度员睡觉的时候添加了一些东西呢?
此解决方案与关键空间事件结合使用。您基本上需要订阅的键,laterChannel
并且每当有添加事件时,您都会唤醒所有调度程序,以便他们可以再次轮询。
然后你有另一个使用阻塞 left pop on 的调度程序nowChannel
。这表示:
- 您可以让调度程序跨多个实例(即它正在扩展)
- 轮询是原子的,因此您不会有任何竞争条件或双重事件
- 任务由任何空闲的实例执行
有一些方法可以进一步优化这一点。例如,不是返回“0”,而是从 zset 中获取下一项并直接返回正确的睡眠时间。
到期
如果不能使用 Lua 脚本,可以在过期文档上使用键空间事件。订阅频道并在 Redis 驱逐它时接收事件。然后,抓住一把锁。这样做的第一个实例会将其移动到列表(“立即执行”通道)。然后你不必担心睡眠和轮询。Redis 会告诉你什么时候该执行某事。
execute_later(timestamp, eventId, event) {
SET eventId event EXP timestamp
SET "lock:" + eventId, ""
}
subscribeToEvictions(eventId) {
var deletedCount = DEL eventId
if (deletedCount == 1) {
// move to list
}
}
然而,这有它自己的缺点。例如,如果您有很多节点,所有节点都会收到事件并尝试获取锁。但我仍然认为这里提出的任何建议总体上都较少。