dot Redis 8 来了——而且它是开源的

了解更多

构建优秀 Redis 模块的 5 个步骤

编写 Redis 模块时需要记住的五件事。虽然这个列表并不详尽,但我的目标是为那些模块构建经验不多的人提供一个好的入门方法。

1. 找到一个引人注目的模块用例

Redis 已经有很多工具可以让你构建所需的精确解决方案。一个例子可以是锁。使用 SET 命令并带上 NX 选项,你可以创建一个锁键,并将其与EXPIRE结合,即可实现锁租约。这在解决协调问题时非常有用。当内置命令不足时,你还可以诉诸于Lua 脚本,它们为复合操作添加了完整的可编程性,这些操作随后会由 Redis 原子地执行。

模块更进一步,由于能够访问比 Lua 更底层的 API,它们提供了更大的灵活性和速度,但维护和分发也更具挑战性。只有当 Lua 无法完全解决你的用例时,才考虑使用模块。

模块可以添加新命令

模块可以为 Redis 添加新命令,这些命令执行任意的 C 函数(准确地说,你也可以使用RustZig或任何 C-ABI 兼容的语言)。你在函数中做什么由你决定。一个基本但有用的起点是实现一个与现有命令类似但功能更多的命令。例如SETNE(由用户首次在这个 GitHub Pull Request中提到)。SETNE 的行为与 SET 完全相同,但当新值与当前值相等时,它不会修改键,从而避免产生虚假的键空间通知。通常,为了获得一些实践经验,可以考虑对现有命令进行一些小改动,以帮助解决特定的用例。 

这些小改动大多数最好实现为 Lua 脚本,但如果你一开始想不出引人注目的模块想法,这也是一个很好的积累经验的方法。留给读者的几个练习:SETEQ、HINCRDATEBY。

模块可以添加新数据类型

模块为 Redis 添加功能的最有效方式是添加新数据类型。Redis 非常注重数据结构及其相关算法和属性的恰当设计。你可能不知道 Set 数据类型的具体实现是什么,但你肯定知道集合成员检查(SISMEMBER)无论 Set 的大小如何总是很快的(也就是说,它的渐近复杂度是次线性的),例如。

要了解关于 BigO / 渐近复杂度和 Redis 数据类型重要性的介绍,请观看 Rob Conery 的这场演讲

这是我们自己模块背后的基础

  • RediSearch 是一个基于倒排索引的全文搜索模块。
  • RedisGraph 是一个基于稀疏矩阵的图数据库模块。
  • RedisTimeSeries 类似于 Redis Streams,但针对数值序列进行了优化。
  • RedisBloom 提供了一些不同的概率数据结构。
  • RedisAI 运行 Tensorflow 深度学习图(以及其他几种类型)。

这些是重要的模块,但并非所有引入新数据类型的模块都必须如此复杂。有很多更简单的数据类型可以作为模块使用。一个基本的例子可以是 Redis 中已有数据类型的不同实现,例如使用 ArrayList 来实现 Lists。

2. 完善你的 API

别忘了,应对模块命令的错误用法与应对正确用法同样重要。Redis 用户喜欢手动尝试命令以更好地理解,输入错误的参数是这个过程的一部分。你的 API 应该易于使用且难以误用,但当不可避免的情况发生时,确保报告有意义的错误消息。

看看 Redis 中标准命令的行为方式,并尝试设计出遵循相同假设的命令。这将减少使用你的命令所需的认知负担。一个例子是,在 Redis 中,大多数命令在针对不存在的键调用时都有合理的行为:INCR会假定缺失的键值为 0,然后将其设置为 1,SADD会假定缺失的键是一个空集合,以此类推。

3. 做个好公民

模块可以与 Redis 生态系统交互。务必阅读文档以了解如何正确处理细节,特别是如果你的模块实现了新的数据类型。以下是两个最重要的方面需要注意。

命令标志

当你声明一个新命令时,你必须指定一些标志以告诉 Redis 你的命令在被调用时会做什么。它是只读数据还是也会写入数据?它会分配内存还是只修改现有数据?务必正确填写这些选项。例如,在内存不足(OOM)的情况下,deny-oom 是一个重要的标志,它会告诉 Redis 拒绝访问分配内存的命令,否则整个进程可能会被 OOM killer 杀死!即使是read-only 标志也很重要。新的客户端缓存功能会用它来决定是否为给定的键启用跟踪。

命令复制

当 Redis 运行在主/副本设置中时,主节点必须知道哪些命令应该发送给副本,哪些不应该。并非所有命令都需要复制,有些命令可能只在特定条件下需要复制。例如,我在上面提到的 SETNE 命令,它只在新值与当前值不同时才会设置键值(否则什么也不做)。在这种情况下,命令只在实际对键应用更改时才应该被复制。如果它不执行任何写入操作,就没有理由让每个副本都执行它。Redis 无法从外部知道该怎么做,因此你必须正确使用RedisModule_ReplicateVerbatim及相关函数。 

4. 编写出色的文档

如果没人知道如何使用你的模块,那么它的有用性就无关紧要了。完善你的 API 在这方面大有帮助,但首先你需要说服潜在用户至少值得尝试你的模块。一个好的模块应该有良好的文档,解释模块的总体目标,并列出每个命令的详细信息。

如果你查看redis.io,你会看到每个命令都列出了其相对的 BigO 复杂度,并且有一些额外的注意事项,说明命令何时具有特别大或小的常数,或者何时存在值得注意的边缘情况。尝试复制这种格式,特别是在命令示例的语法方面。注意每个示例如何使用小写名称作为占位符,而大写名称表示必须按原样使用的关键字,可选值则放在方括号内。查看SET 命令的文档以查看示例。

最重要的是:力求简洁

始终记住,Redis 背后的第一设计原则是简洁。这并不意味着你的模块不应探索其他选项,也不应偶尔为其他好处牺牲简洁性(模块存在的目的正是为了让 Redis 用户进行实验),但务必始终注意你正在放弃什么。

一般来说,当你为了易用性牺牲简洁性时,你也在隐式地限制用户使用你的模块的方式。在 Redis 中,大多数实用性通常不是来自于孤立使用的单个命令,而是来自于用户如何将不同的命令组合在一起。更小、更清晰、更简单的命令总是更容易组合,从而在总体上产生更大的效果。因此,我建议在诉诸这种权衡之前,先通过适当应用上述技术来提高易用性。

另一个潜在的权衡可能有利于效率。这可能值得探索,也是 Redis 自己偶尔会做出的权衡。一些内置数据类型有两种内部表示形式——一种针对数据类型只包含少量元素时进行了优化,另一种则用于键增长超过某个阈值时。两种表示形式(加上两者之间的切换机制)肯定比一种更复杂,但收益可能值得。这一点尤其正确,因为增加的复杂性不会体现在用户界面上,用户无论使用哪种内部表示形式都会以相同的方式与数据类型交互。

总结

查看已存在的模块,看看能否找到灵感。我们发布了一个用于使用 Rust 编写模块的 SDK,还写了关于使用 Zig 实现的文章,所以如果你不(想)了解 C 语言,也不必担心。我们在 YouTube 上也有演讲(RustZig),如果你更喜欢听而不是读。

如果你最终编写了一个模块,请务必向antirez/redis-doc发送一个 pull request,以便将其添加到redis.io上;如果你愿意,也可以在 Twitter 上给我发条消息 @croloris。我很乐意试用你的模块。