编写 Redis 模块时应注意五件事.虽然此清单并非详尽无遗,但我的目的是如果您尚未在模块构建方面拥有太多经验,它可提供一种启动的好方法。
Redis 已拥有大量工具,让您能够构建自己需要的精确解决方案。一个示例可能是锁。通过将 SET 与 NX 选项结合使用,就能够创建一个锁键,并将其与EXPIRE结合使用,就能获得锁订用租约。这在解决协调问题时会非常有用。如果内置命令不够用,您还可以使用Lua 脚本,它为 Redis 原子执行的复合操作添加了完整的可编程性。
与 Lua 相比,模块更进一步,为您提供了更大的灵活性和速度,因为它们有能力访问更底层的 API,但是维护和分发它们更有挑战性。仅当 Lua 无法完全解决您的用例时,才使用模块。
模块可以向 Redis 添加新命令,这些命令执行任意 C 函数(准确来说,您还可以使用Rust、Zig或任何与 C-ABI 兼容的语言)。您在函数中做什么取决于您。一个基本的但有用的起点是实现一个与现有命令相似但执行其他操作的命令。一个示例可能是SETNE(这最早由一个用户在这GitHub Pull Request 中提到)。SETNE 的行为完全像 SET,但是当新值等于当前值时,它不修改键,从而避免产生虚假的键空间通知。一般来说,为了获得一些练习,可以考虑对现有命令进行一些小的补充,以帮助解决特定用例。
这些小的补充中的大多数最好作为 Lua 脚本实现,但如果您一开始想不出引人注目的模块创意,这是一种获得一些经验的好方法。留给读者的几个练习:SETEQ、HINCRDATEBY。
模块添加 Redis 功能的最有效方式是添加新的数据类型。Redis 非常注重数据结构及其相关算法和属性的恰当设计。虽然你可能不知道 Set 数据类型的具体实现方式,但你一定知道,无论 Set 的大小如何(例如,它具有次线性渐近复杂度),集合成员关系(SISMEMBER)始终都很快。
这是我们自己模块的基础
这些都是重要的模块,但并非每个引入新数据类型的模块都必须如此复杂。有许多可以作为模块使用的更简单的数据类型。一个基本的示例可能是对 Redis 中已存在数据类型采用不同的实现方式,例如使用 ArrayList 来实现 List。
请不要忘记,为模块命令的错误用法做好准备和为正确用法做好准备同样重要。Redis 用户喜欢手动尝试命令以获得更好的理解,而输入错误的参数也是该过程的一部分。你的 API 应易于使用且难以误用,但在不可避免地发生这种情况时,请务必报告有意义的错误消息。
了解在 Redis 中如何使用标准命令,并尝试找到遵守相同假设的内容。这将减少使用这些命令所需的心理开销。一个示例是,在 Redis 中,在不存在密钥时,大多数命令的行为都很明智: INCR 将假设缺失的密钥的值为 0,因此会将值设置为 1, SADD 将假设缺失的密钥是空集合,依此类推。
Redis 模块可以与 Redis 生态系统进行交互。 请务必阅读文档,以了解如何正确获取详细信息,尤其是当模块实现一个新数据类型时。以下是两个最重要的方面,以确保正确性。
声明新命令时, 必须指定几个标记,以告诉 Redis 在调用命令时所要做什么。是仅仅读取数据还是写入数据?是要分配内存还是仅仅修改现有数据?确保正确填写这些选项。例如,在内存不足 (OOM) 情况下, deny-oom 是一个重要的标记,它会告诉 Redis 拒绝访问分配内存的命令,否则整个进程都将被 OOM 终止器终止!甚至 read-only 标记也很重要。 新的客户端缓存功能 会用它来决定是否启用对给定密钥的跟踪。
当 Redis 在主从设置中运行时,主服务器必须知道它应该发送给从服务器的命令。并非每个命令都应复制,而一些命令可能仅在特定条件下才需要复制。例如,我在上面提到了 SETNE 命令,它仅在新的值与当前值不同时才会设置密钥值(否则它什么都不做)。在这种情况下,只应在对密钥有效应用更改时复制该命令。如果没有执行任何写入,则让每个从服务器都执行该命令没有任何理由。Redis 无法从外部得知该怎么做,因此必须正确使用 RedisModule_ReplicateVerbatim 和相关函数。
无论你的模块多么有用,如果没有明白如何使用它,还是没有意义。打磨你的 API 在这方面可以有很大帮助,但首先你需要说服潜在用户这个模块至少值得一试。一个好的模块应有良好的文档说明模块的总体目标并列出每个指令的详细信息。
如果你访问一下 redis.io,你将会看到每个指令都列出了其相对 BigO 复杂度并为指令具有特别大或小的常量或当存在显著的边界案例时提供了额外注释。尝试复制这种格式,特别是在指令示例的具体语法中。请注意,每个示例对于占位符使用小写名称,而大写名称表示必须逐字使用的关键词,同时方括号内表示可选值。查看 SET 的文档,了解此示例。
始终牢记,Redis 背后的第一个设计原则是简单性。这并不意味着你的模块永远不能探索其他选项并偶尔牺牲简单性以换取其他好处(模块的存在正是为了让 Redis 用户进行实验),但始终要谨记你放弃了什么。
一般来说,当为了易用性而牺牲简单性时,实际上也在隐含地限制了你的用户使用你的模块的方式。在 Redis 中,大多数实用性通常不是来自单独使用某个指令,而是来自用户如何将不同的指令结合在一起。较小、明确、简单的指令将始终更容易结合,从而在宏伟的事物构想中产生更大的成果。出于此原因,我建议在诉诸这种权衡之前,通过适当地应用上述技术来提高易用性。
另一个潜在的权衡可能是效率。这可能值得探索,也是 Redis 偶尔会进行的权衡。一些内置的数据类型具有两个内部表示形式——一种针对数据类型只有少量元素时进行了优化,而另一种则是针对数据类型增长超过某一阈值的情况。两个表示形式(加上在这两个表示形式之间切换的机制)当然比一个更复杂,但好处可能是值得的。这尤其如此,因为用户无论使用哪个内部表示形式都会以相同的方式与数据类型交互,因此这种增加的复杂性不会在用户界面中显现出来。
查看 哪些模块已经存在,看看你是否能从中获得灵感。我们发布了用 Rust 编写的模块 SDK,还用 Zig 编写了有关该模块的文章,因此如果你(不想)了解 C 也别担心。如果你更喜欢听而不是读,我们还在 YouTube 上有演讲(Rust、Zig)。
如果你最终写了一个模块,请务必向antirez/redis-doc发送请求,以将其添加到redis.io中,如果你有兴趣,可以向我发送推文 @croloris。我将试着使用你的模块。