Redis Loadable Modules System

Salvatore Sanfilippo

Redis 可加载模块系统

这只是时间问题,但它最终还是发生了。7 年前,在 Redis 1.0 的发布说明中,我提到未来一个有趣的功能是“loadable modules(可加载模块)”。那时我确实非常期待这一功能,但这些年来,我越来越怀疑是否应该在 Redis 中加入可加载模块。也许这确实有充分的理由。

模块可能是一个系统最有趣、同时也是最棘手的功能:不同版本之间的 API 不兼容、低质量模块导致系统崩溃,以及一个可扩展系统缺乏自身特性,都可能成为问题。因此,多年来我一直设法避免向 Redis 添加模块,而 Lua 脚本是延迟这项工作的一个好工具。与此同时,多年的脚本使用经验表明,脚本可以用来“组合”已有功能,却无法将系统的能力扩展到那些系统原本并未设计来支持的使用场景。

此前对模块的尝试也表明,将 Redis 与可加载模块混合在一起时,主要痛点之一在于模块与 Redis 核心的绑定方式。在很多方面,Redis 更像是一门编程语言,而不是一个数据库。要正确扩展 Redis,模块需要访问系统的内部 API。直接向模块导出 Redis 核心函数会造成巨大的问题:模块开始依赖 Redis 的内部细节。一旦 Redis 核心发生演进,模块就需要重写。这要么会造就一个脆弱的模块生态,要么会阻碍 Redis 核心的发展。Redis 内部机制不可能停止演进,模块开发者也不可能为了跟上内部变化而不断修改模块(过去某些流行系统中就发生过类似情况,结果很糟糕)。

带着这些经验,我离开卡塔尼亚,飞往特拉维夫,准备在 Redis Labs 开会,讨论未来几个月的路线图。我们讨论的话题之一就是可加载模块。在飞行途中,我问自己,是否有可能真正将 Redis 核心与模块 API 解耦,同时仍然能够以较低层次直接操作 Redis 的数据结构。于是我立即开始编写代码。我希望未来的 API 具有极高的兼容性,这样今天编写的模块在 4 年后仍能使用同一套 API 工作,而不受 Redis 核心变化的影响。我还希望具备 binary compatibility(二进制兼容性),这样这个编写于 4 年前的模块甚至可以直接在新版 Redis 中加载并按预期运行,无需重新编译。

飞行结束时,我抵达特拉维夫,而“modules”分支中的东西已经可以运行了。我们一起讨论了 API 的工作方式,最后所有人都同意,能够直接操作 Redis 内部机制是一项基础功能。我们想实现的是,让 Redis 开发者能够创建出功能与 Redis 原生命令一样强大、速度也一样快的命令。仅靠调用 Redis 命令的高级 API 无法做到这一点,因为它太慢且受到限制。一个只能做 Lua 已经能做的事情的 Redis 模块系统没有意义。你必须能够说:获取与这个键关联的值,它是什么类型?对这个值执行这个底层操作。给我一个位于此位置的有序集合游标,移动到下一个元素,依此类推。要创建一个作为这类底层访问中间层的 API 很棘手,但绝对可行。

我回到家后立即开始开发模块系统。几周后,我已经有了一个足够实用的原型,可以用来开发有趣的模块,其中包括数据类型底层访问等底层函数、必要时通过字符串 DMA 在没有包装器的情况下直接操作字符串内部机制的功能、复制 API、有趣的有序集合迭代器 API,等等。开局看起来非常有希望,不过这个项目有点“秘密”,因为当时还不清楚它最初是否可行。此外,我们也希望避免在 API 极不稳定、可能随时变化时,所有人就开始开发模块。

这一过程的成果虽然尚未完成,但非常令人期待。因此,今天我在 Redis Conference 2016 上宣布了这一新功能,代码刚刚被推送到“unstable”分支。不过,让我们先简单看看 API……

下面是一个模块可以做什么以及它如何工作的简单示例。它实现了一个“list splice”操作,将元素从一个列表移动到另一个列表:

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。例如,系统支持 automatic memory management(自动内存管理):命令运行所处的上下文会收集用户未显式释放的对象,并在命令返回时根据需要将其释放。这让模块编写变得简单得多。

你可以在这里找到 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)一起发布。不过,它已经足够完成很多事情了,我的同事们已经做出了非常有趣的东西,从 inverted indexes(倒排索引)到身份验证系统。接下来几周我们会补齐所有缺口。例如,目前还没有底层 Set API,所以暂时必须使用 Call() 风格的 API。类似地,目前迭代器只为 sorted set 类型提供,等等。

但重要的是,这个过程已经开始,Redis 正在成为一个可扩展的系统。我认为,这将赋予 Redis 用户更大的力量,让他们在使用 Redis 建模和解决问题时能够“跑在”项目本身的前面;更大的承诺是,在 Redis 4.0 RC 发布之后,多年以来我们将不会再破坏 API,这样模块开发者的工作就不会付诸东流。需要注意的是,我们仍然可以改进 API,因为模块注册时会请求指定的 API 版本。因此,在发布新版 API 的同时,仍然能够保持向后兼容。

很快就会有一个 Modules Directory,你可以使用 redis-cli,将自己的模块注册到一个通过 Redis 协议通信的服务器中。遗憾的是,我们没能及时完成它,但这只是几周之内的事情。

我对接下来会发生的一切感到非常非常兴奋!模块将像客户端一样采用 Bazar model(集市模式),因此不会有“官方模块”。好的模块一定会被使用,而且所有模块都会列在 Redis 网站上,可能会按照 Github stars 之类的指标进行排名。

我希望许多用户能够开始参与模块生态,让 Redis 能够解决那些不适合在核心中解决、但非常适合通过模块解决的特定使用场景。

现在 API 仍然具有很大的可调整空间,我需要你们的反馈。告诉我你们的想法!