跳转到主要内容

KEYDB.CRON:用你的数据库调度 Lua 脚本

为您的数据库引入 Cron 的强大功能#

在设置数据库时,Cron 通常就在不远处... 所以对于低延迟的调用,为什么不让数据库来处理呢?

我们最近通过 KEYDB.CRON 命令引入了这个工具,它允许用户通过调度器在特定时间或间隔执行 Lua 脚本。此功能是持久化的,并存储在本地,将调用带入数据库内部,而不是通过第三方工具。

对于不了解的人,KeyDB 是一个比 Redis 更快、多线程的替代品。我们努力引入强大的工具和功能,为用户降低复杂性。KEYDB.CRON 是 KeyDB 开发的众多功能之一。

Cron 应该由数据库运行吗?#

在设置数据库服务器时,您希望尽量减少需要监控和维护的进程数量——KEYDB.CRON 正是为此而生。外部设置 Cron 是另一个需要配置和验证的工具。时钟需要同步,时区需要设置。拥有一个内置工具,其功能会自动传递给新的副本节点并随数据库迁移,可以减少潜在错误并简化设置。

话虽如此,关于在 KeyDB/Redis 内部运行 Lua 脚本以及这些代码应存放于何处,可以进行长篇辩论。如果您在这种阻塞调用上运行冗长或不可预测的脚本,可能会产生问题并影响性能。然而,这个论点更多是关于“何时使用 Lua”,而不一定是关于使用内部调度器执行这些任务是否是个好主意。这些争论通常归结为工程问题,但正确使用一个额外的工具可以带来很多好处,包括避免与远程客户端交互所带来的延迟和网络开销。

KEYDB.CRON 将 Lua 脚本存储为一个 KEY 并持久化到数据库中,因此如果服务器重启、创建副本节点或迁移数据,它都仍然存在。该命令基于 Unix 时间进行调度以保证一致性,并且可以调度单个任务或精确到毫秒的周期性任务。

我应该何时在数据库中使用 Cron?#

如果您曾在 KeyDB 或 Redis 中使用过 Lua,KEYDB.CRON 可能是您手中一个强大的工具。许多脚本是由事件或请求触发的,但也有很多任务需要定期或在特定时间/间隔执行。使用 KEYDB.CRON 将调度器引入数据库内部,而不是放在客户端或作为第三方调度器。KEYDB.CRON 的功能类似于使用 EVAL,但增加了内置调度器的额外功能。

您可以运行精确到毫秒的周期性任务,或对数据库执行一次性计划任务,不过具体做什么很可能取决于您的用例。有些人使用 Lua 脚本 + Cron 来做诸如以下的事情:

  • 调度过期、批量清理和垃圾回收
  • 数据库的定期维护和整理
  • 维护/重置计时器、计数器
  • 从列表中弹出项目、检查超时等
  • 作为任务队列的一部分
  • 数据处理、整合
  • 执行特定于应用程序的驱逐等任务
  • 确保一次性任务在精确时间执行
  • ... 以及任何其他适用的情况(我们很想听听您的用例:https://community.keydb.dev

使用 KEYDB.CRON 命令:#

您也可以参考我们的文档这里,以获取有关使用该命令的参考信息。

基本原理非常简单

KEYDB.CRON name [single/repeat] [optional: start] delay script numkeys [key N] [arg N]

其中

  • name 是 KEY 的名称。它将在键空间中可见,可以被搜索,并用 DEL 删除。每个 Cron 任务都有自己的名称。
  • [single/repeat] 指定脚本是只运行一次,还是按指定间隔重复运行。
  • [optional: start] 是一个以毫秒为单位的整数,表示自 Epoch 以来的时间。如果指定,脚本将直到达到此 Unix 时间后才会执行。如果延迟大于零,则在脚本执行前需要经过这段延迟时间(计时器在开始时间启动)。如果指定了开始时间,延迟将始终参照该开始时间进行间隔计算。
  • delay 是一个以毫秒为单位的整数,用作初始延迟。如果指定了 repeat,它也将是重复计时器的时长,每次延迟结束时都会执行脚本(将无限期地继续执行)。
  • script 是要执行的 Lua 脚本。这应该是 Lua 脚本本身,而不是已加载脚本的 SHA1 摘要(尚不支持)。
  • numkeys [key N] [arg N] 是键的数量、键和脚本的参数,用法与 EVAL 类似。

持久性:#

与可能仅被加载(仅缓存)的传统 Lua 脚本不同,如果保存了 KEYDB.CRON 任务,它将在服务器重启后依然存在。它可以作为键空间中的一个 KEY 被查看、删除或修改。

示例:#

keydb-cli> KEYDB.CRON mytestname REPEAT 1610941618000 60000 "redis.call('INCR',KEYS[1])" 1 mytestcounter
OK

上面的示例从 Unix 时间戳 1610941618000 毫秒开始,每 60 秒增加一次 'mytestcounter' 的值。

keydb-cli> KEYDB.CRON mytestname2 SINGLE 1610941618000 1 "redis.call('set',KEYS[1],'0')" 1 mytestcounter
OK

上面的命令在 Unix 时间戳 1610941618000 毫秒时将 "mytestcounter" 设置为零。这只会发生一次。请注意,我们必须指定一个延迟时间。任务完成后,该 KEY 将被移除,名称 "mytestname2" 将不再存在。

如果您想删除 Cron 任务,只需删除作为 Cron 任务名称的 KEY,该功能将不再存在。

keydb-cli> DEL mytestname
(integer) 1

从文件加载脚本#

对于更冗长的任务,我们可能希望用指定的输入调用一个已存在的 Lua 脚本。

示例#

让我们先创建一个简单的 Lua 脚本,myscript.lua。当集合成员超过 100 个时,此脚本将查看集合中排名最低的 3 个成员。在本例中,我们希望移除任何在最低 3 名排名中出现超过 10 次的成员。因此,我们将跟踪任何低排名的成员(也用于客户端筛选)。一旦成员被移除,我们将在 24 小时后使其计数器过期以重置访问。

myscript.lua#

if redis.call("ZCARD", KEYS[1]) > 100 then
--找到排名最低的3个成员
local lowest_three = redis.call("ZRANGE", KEYS[1], "0", "2")
for i in ipairs(lowest_three)
do
--如果处于最低3名,则增加该成员的特定计数器
local counter = "low_count:" .. lowest_three[i]
local new_count = redis.call("INCR", counter)
--在24小时后使计数器过期
redis.call("EXPIRE", counter, "86400")
if new_count >= 10 then
redis.call("ZREM", KEYS[1], lowest_three[i])
end
end
end

现在我们希望每分钟运行一次此脚本,以探测低排名的成员。我们将 Cron 任务命名为 "mycrontask",并将其传递给有序集合的名称 "rankings",我们的脚本将通过 KEY[1] 来引用它。

现在我们通过客户端使用以下命令将 Lua 脚本加载到 Cron 任务中

keydb-cli -p 6379 KEYDB.CRON mycrontask repeat 60000 "$(cat myscript.lua)" 1 rankings

重要注意事项:#

以下是使用 KEYDB.CRON 时的一些注意事项

  • Lua 脚本是阻塞的,因此确保它们不等待任何东西,也不是冗长或易出错的,这一点非常重要。理想情况下,执行时间应保持在亚毫秒级。
  • 根据您对 Lua 和 Cron 的使用和应用,您应该运行基准测试以确保对性能没有不利影响。
  • 没有简单的方法来识别哪些存储的键是脚本以及这些脚本的定义,因此给它们起好名字并做好文档记录非常重要。

我可以在 Redis 中使用 Cron 吗?#

此功能不属于 Redis。但是,KeyDB 是 Redis 的一个直接替代品,与 Redis 模块、API 和协议完全兼容。这包括 Lua 脚本和 Redis 客户端。KeyDB 可以被看作是功能的超集,增加了额外的功能和配置选项以及命令。由于 KeyDB 的多线程架构,我们能够提供强大的工具,而这些工具对于单线程应用程序来说可能过于繁重。

保持对 KEYDB 的关注#

我们正在开发一些非常酷的功能。要了解我们的最新动态,请关注我们的以下渠道之一