Redis 可加载模块系统
原文由 Salvatore Sanfilippo 于 发布,订阅该博客
迟早会发生的事,终究还是发生了。7 年前在 Redis 1.0 的发布说明里,我曾提到未来一个值得关注的特性就是“可加载模块”。当时我对这个功能确实很感兴趣,但这些年里,我对在 Redis 中加入可加载模块的想法却越来越持怀疑态度。或许,这是有充分理由的。
模块既可能成为一个系统最有意思的特性,也可能同时成为最棘手的问题:版本间的 API 不兼容、低质量模块导致系统崩溃、可扩展系统缺乏明确定位,都是可能出现的问题。因此,多年来我一直设法避免为 Redis 加入模块,而 Lua 脚本则成了推迟这一决定的好工具。与此同时,多年使用脚本的经验表明,脚本是一种“组合”已有功能的方式,却无法将系统的能力扩展到那些并非为其设计的用例上。
之前对模块的尝试也表明,将 Redis 与可加载模块结合时,最主要的痛点之一在于模块与 Redis 核心的绑定方式。在很多方面,Redis 更像一门编程语言,而非数据库。要真正扩展 Redis,模块需要能够访问系统的内部 API。直接将 Redis 核心函数暴露给模块会带来巨大的问题:模块会开始依赖 Redis 的内部细节。一旦 Redis 核心演进,模块就得重写。这要么会造就一个脆弱的模块生态,要么会阻碍 Redis 核心本身的演进。Redis 的内部实现不可能停止演进,模块开发者也不可能为了跟上内部变化而不断修改模块(过去在某些流行系统中就曾发生过这种情况,结果并不理想)。
带着这些教训,我从卡塔尼亚出发飞往特拉维夫,准备在 Redis Labs 开会讨论未来几个月的路线图。我们聊的话题之一就是可加载模块。在飞行途中,我自问是否有可能在真正将 Redis 核心与模块 API 解耦的同时,依然保留直接操作 Redis 数据结构的底层访问能力。于是我立刻开始写起了代码。我想要的是面向未来极高程度的 API 兼容性:今天编写的模块,4 年后依然能用同一套 API 正常工作,而不管 Redis 核心发生了怎样的变化。我还想要二进制兼容性,让 4 年前的模块甚至无需重新编译,就能在新版 Redis 中直接*加载*并按预期运行。
飞行结束抵达特拉维夫时,我在“modules”分支上已经有了可以运行的东西。我们一起讨论了 API 该如何设计,最终大家一致认为,能够直接操作 Redis 内部数据是一项基础能力。我们想实现的是,让 Redis 开发者能够创建出与原生命令一样强大、一样快的命令。这光靠一个调用 Redis 命令的高层 API 是做不到的,那样太慢也太受限。如果 Redis 模块系统只能做到 Lua 已经能做的事,那就没有意义了。你需要能够做到:给我这个键对应的值,它是什么类型?对这个值执行这种底层操作。给我一个指向有序集合中这个位置的游标,移到下一个元素,以此类推。要为这种底层访问创建一个作为中间层的 API 确实棘手,但完全可行。
回到家后,我立刻着手开发这套模块系统。几周后,我就拿出了一个已足够开发有趣模块的原型,具备了诸如数据类型的底层访问、必要时可绕过封装直接操作字符串内部的 strings DMA、复制 API、有意思的有序集合迭代器 API 等底层功能。看起来这是一个非常有前景的开端,不过这个项目在某种程度上是“保密”进行的,因为一开始还不清楚它是否可行。而且我们也想避免在 API 极不稳定、随时可能变动的时候,就让所有人都开始开发模块。
这个过程的成果虽然尚未完成,但已经非常令人期待,所以今天我在 Redis Conference 2016 上公布了这一新特性,代码也刚刚被推送到了“unstable”分支。不过,我们先来看看 API 是什么样的……
下面是一个模块能做什么以及如何工作的简单示例。它实现了一个“列表拼接”操作,将元素从一个列表移动到另一个列表:
int HelloListSpliceAuto_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 4) return RedisModule_WrongArity(ctx);
RedisModule_AutoMemory(ctx);
RedisModuleKey *srckey = RedisModule_OpenKey(ctx,argv[1],
REDISMODULE_READ|REDISMODULE_WRITE);
RedisModuleKey *dstkey = RedisModule_OpenKey(ctx,argv[2],
REDISMODULE_READ|REDISMODULE_WRITE);
/* Src and dst key must be empty or lists. */
if ((RedisModule_KeyType(srckey) != REDISMODULE_KEYTYPE_LIST &&
RedisModule_KeyType(srckey) != REDISMODULE_KEYTYPE_EMPTY) ||
(RedisModule_KeyType(dstkey) != REDISMODULE_KEYTYPE_LIST &&
RedisModule_KeyType(dstkey) != REDISMODULE_KEYTYPE_EMPTY))
{
return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
}
long long count;
if ((RedisModule_StringToLongLong(argv[3],&count) != REDISMODULE_OK) ||
(count < 0))
{
return RedisModule_ReplyWithError(ctx,"ERR invalid count");
}
while(count-- > 0) {
RedisModuleString *ele;
ele = RedisModule_ListPop(srckey,REDISMODULE_LIST_TAIL);
if (ele == NULL) break;
RedisModule_ListPush(dstkey,REDISMODULE_LIST_HEAD,ele);
}
size_t len = RedisModule_ValueLength(srckey);
RedisModule_ReplyWithLongLong(ctx,len);
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx) {
if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"hello.list.splice.auto",
HelloListSpliceAuto_RedisCommand,
"write deny-oom",1,2,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
}为了提供一套简洁且不易被误用的 API,我们下了很大功夫。例如,系统支持自动内存管理,命令所处的上下文会自动收集那些未被用户显式释放的对象,并在命令返回时按需释放它们。这让编写模块变得简单得多。
你可以在这里找到 API 文档(尚不完美,但已足够入门了解):
https://github.com/antirez/redis/blob/unstable/src/modules/INTRO.md
API 参考在这里:
https://github.com/antirez/redis/blob/unstable/src/modules/API.md
以及许多简单的命令示例在这里:
https://github.com/antirez/redis/blob/unstable/src/modules/helloworld.c
API 目前既不完整也不稳定,将随 Redis 的下一个稳定版本(可能是 4.0)一起发布。不过,它已经足以做很多事情了,我的同事们也已经用它做了不少有意思的东西,从倒排索引到认证系统都有。在接下来的几周里,我们会补上所有缺口,例如目前还没有 Set 类型的底层 API,所以暂时只能使用 Call() 风格的 API。同样,迭代器目前也只为有序集合类型提供了,诸如此类。
但重要的是,这一进程已经启动,Redis 正在成为一个可扩展的系统。我认为这会赋予 Redis 用户更强的能力,让他们在使用 Redis 建模自身问题时,能够比项目本身“跑得更快”,并且我们郑重承诺,在 Redis 4.0 RC 发布之后,未来数年内我们都不会破坏 API,这样大家在模块上的投入就不会白费。请注意,我们*可以改进* API,因为模块在注册时会声明所需的 API 版本,所以我们完全可以在保持向后兼容的同时发布新版本的 API。
很快就会有一个模块目录,你可以通过 redis-cli 将自己的模块注册到一个使用 Redis 协议的服务器上。遗憾的是没能及时完成,但也就是几周内的事了。
我对接下来会发生的一切感到非常非常兴奋!模块将像客户端那样采用集市模式,所以不会有所谓的“官方模块”。优秀的模块自然会被大家采用,所有模块都会在 Redis 官网上列出,可能会按 GitHub star 数之类的指标来排序。
我希望许多用户能开始加入模块生态,让 Redis 能够解决那些在核心中实现并不明智、却非常适合用模块来解决的特定场景。
现在 API 仍具可塑性,我需要你们的反馈。请告诉我你们的想法!
随机一篇博客
评论
登录后参与讨论