我需要设计一个Redis驱动的可扩展任务调度系统。
要求:
伪API:schedule_task(timestamp, task_data)
。时间戳是整数秒。
基本理念:
到目前为止,我无法弄清楚如何在Redis原语中使用它...
任何线索?
请注意,有一个类似的老问题:Delayed execution / scheduling with Redis?在这个新问题中,我介绍了更多细节(最重要的是,许多工人)。到目前为止,我无法弄清楚如何在这里应用旧答案 - 因此,这是一个新问题。
答案 0 :(得分:9)
这是另一种基于其他几种解决方案的解决方案[1]。它使用redis WATCH命令删除竞争条件,而不使用redis 2.6中的lua。
基本方案是:
我没有测试过: - )
foo工作创建者会:
def schedule_task(queue, data, delay_secs):
# This calculation for run_at isn't great- it won't deal well with daylight
# savings changes, leap seconds, and other time anomalies. Improvements
# welcome :-)
run_at = time.time() + delay_secs
# If you're using redis-py's Redis class and not StrictRedis, swap run_at &
# the dict.
redis.zadd(SCHEDULED_ZSET_KEY, run_at, {'queue': queue, 'data': data})
schedule_task('foo_queue', foo_data, 60)
调度员看起来像:
while working:
redis.watch(SCHEDULED_ZSET_KEY)
min_score = 0
max_score = time.time()
results = redis.zrangebyscore(
SCHEDULED_ZSET_KEY, min_score, max_score, start=0, num=1, withscores=False)
if results is None or len(results) == 0:
redis.unwatch()
sleep(1)
else: # len(results) == 1
redis.multi()
redis.rpush(results[0]['queue'], results[0]['data'])
redis.zrem(SCHEDULED_ZSET_KEY, results[0])
redis.exec()
foo工作人员看起来像:
while working:
task_data = redis.blpop('foo_queue', POP_TIMEOUT)
if task_data:
foo(task_data)
[1]此解决方案基于not_a_golfer,一个位于http://www.saltycrane.com/blog/2011/11/unique-python-redis-based-queue-delay/,另一个基于事务的redis文档。
答案 1 :(得分:7)
您没有指定您正在使用的语言。你至少有3种方法可以做到这一点,而不用在Python中编写一行代码。
Celery有一个可选的redis代理。 http://celeryproject.org/
resque是一个非常流行的使用redis的redis任务队列。 https://github.com/defunkt/resque
RQ是一个简单的小型基于redis的队列,旨在“从celery和resque中获取好东西”,并且更加简单易用。 http://python-rq.org/
如果你不能使用它们,你至少可以看看它们的设计。
但要回答你的问题 - 你想要的是用redis完成的。实际上我过去或多或少都写过。
编辑: 至于在redis上建模你想要的东西,这就是我要做的:
使用时间戳排队任务将由客户端直接完成 - 您将任务放在一个有序集合中,时间戳作为分数,任务作为值(参见ZADD)。
< / LI>中央调度程序每N秒唤醒一次,检查此集合上的第一个时间戳,如果有任务准备好执行,它会将任务推送到“现在要执行”列表。这可以通过“等待”排序集上的ZREVRANGEBYSCORE来完成,获取时间戳&lt; = now的所有项目,这样您就可以立即获得所有准备好的项目。推动由RPUSH完成。
工作人员在“现在要执行”列表中使用BLPOP,当有东西可以工作时唤醒,并做他们的事情。这是安全的,因为redis是单线程的,没有2名工作人员会完成同样的任务。
完成后,工作人员将结果放回响应队列,由调度员或其他线程检查。您可以添加“待处理”存储桶以避免故障或类似情况。
所以代码看起来像这样(这只是伪代码):
客户端:
ZADD "new_tasks" <TIMESTAMP> <TASK_INFO>
调度:
while working:
tasks = ZREVRANGEBYSCORE "new_tasks" <NOW> 0 #this will only take tasks with timestamp lower/equal than now
for task in tasks:
#do the delete and queue as a transaction
MULTI
RPUSH "to_be_executed" task
ZREM "new_tasks" task
EXEC
sleep(1)
我没有添加响应队列处理,但它或多或少像worker:
工人:
while working:
task = BLPOP "to_be_executed" <TIMEOUT>
if task:
response = work_on_task(task)
RPUSH "results" response
编辑:无状态原子调度员:
while working:
MULTI
ZREVRANGE "new_tasks" 0 1
ZREMRANGEBYRANK "new_tasks" 0 1
task = EXEC
#this is the only risky place - you can solve it by using Lua internall in 2.6
SADD "tmp" task
if task.timestamp <= now:
MULTI
RPUSH "to_be_executed" task
SREM "tmp" task
EXEC
else:
MULTI
ZADD "new_tasks" task.timestamp task
SREM "tmp" task
EXEC
sleep(RESOLUTION)
答案 2 :(得分:1)
如果您正在寻找Java上的现成解决方案。 Redisson适合您。它允许使用熟悉的Redisson nodes api并基于Redis队列在ScheduledExecutorService上以分布式方式安排和执行任务(使用cron-expression支持)。
这是一个例子。首先使用java.lang.Runnable
接口定义任务。每个任务都可以通过注入的RedissonClient
对象访问Redis实例。
public class RunnableTask implements Runnable {
@RInject
private RedissonClient redissonClient;
@Override
public void run() throws Exception {
RMap<String, Integer> map = redissonClient.getMap("myMap");
Long result = 0;
for (Integer value : map.values()) {
result += value;
}
redissonClient.getTopic("myMapTopic").publish(result);
}
}
现在已准备好将其汇总到ScheduledExecutorService
:
RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
ScheduledFuture<?> future = executorService.schedule(new CallableTask(), 10, 20, TimeUnit.MINUTES);
future.get();
// or cancel it
future.cancel(true);
cron表达式的示例:
executorService.schedule(new RunnableTask(), CronSchedule.of("10 0/5 * * * ?"));
executorService.schedule(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));
executorService.schedule(new RunnableTask(), CronSchedule.weeklyOnDayAndHourAndMinute(12, 4, Calendar.MONDAY, Calendar.FRIDAY));
所有任务都在Redisson node上执行。
答案 3 :(得分:0)
综合方法似乎有道理:
没有新任务时间戳可能小于当前时间(如果更少则钳位)。假设可靠的NTP同步。
所有任务都以密钥形式进入存储桶列表,后缀为任务时间戳。
此外,所有任务时间戳都转到专用zset(键和分数 - 时间戳本身)。
客户可以通过单独的Redis列表接受新任务。
循环:通过zrangebyscore ... limit获取最早的N个过期时间戳。
BLPOP,其中包含新任务列表上的超时和已获取时间戳的列表。
如果有旧任务,请处理它。如果是新的 - 添加到存储桶和zset。
检查已处理的存储桶是否为空。如果是这样 - 从zset删除列表和entrt。可能不会检查最近过期的存储桶,以防止时间同步问题。结束循环。
批判?评论?替代?
答案 4 :(得分:0)
我做了一些类似于这里建议的东西,但优化了睡眠持续时间以更精确。如果您对延迟任务队列的插入很少,则此解决方案很好。这是我使用 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
的键,每当有添加事件时,您就会唤醒所有调度程序,以便他们再次轮询。
然后你有另一个调度器使用 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
}
}
然而,这有其自身的缺点。例如,如果您有很多节点,它们都会收到事件并尝试获取锁。但我仍然认为这里的任何建议的要求总体上都较少。