3.7 Redis Modules (模块) (Redis 4.0+) 3.7 Redis Modules (模块) (Redis 4.0+) 详解与代码实践 Redis 从 4.0 版本开始引入了模块 (Modules) 功能,这是一个革命性的进步,极大地扩展了 Redis 的能力和应用场景。模块允许开发者使用 C 或其他语言编写自定义的功能,并将其动态加载到 Redis 服务器中,从而实现对 Redis 功能的扩展和定制。 这使得 Redis 不再仅仅是一个键值存储系统,而成为了一个更加灵活和强大的数据平台。 3.7.1 Redis 模块的核心概念 在深入代码实践之前,我们需要理解 Redis 模块的一些核心概念: 1.
Redis 从 4.0 版本开始引入了模块 (Modules) 功能,这是一个革命性的进步,极大地扩展了 Redis 的能力和应用场景。模块允许开发者使用 C 或其他语言编写自定义的功能,并将其动态加载到 Redis 服务器中,从而实现对 Redis 功能的扩展和定制。 这使得 Redis 不再仅仅是一个键值存储系统,而成为了一个更加灵活和强大的数据平台。
在深入代码实践之前,我们需要理解 Redis 模块的一些核心概念:
1. 模块 (Module) 的本质:
模块本质上是一个动态链接库 (.so 文件,在 Windows 上是 .dll 文件)。
模块使用 C 语言编写,并遵循 Redis 模块 API 规范。
模块在 Redis 服务器启动时或运行时动态加载。
2. 模块提供的能力:
新的数据类型 (Data Types): 模块可以定义全新的数据类型,例如,你可以创建一个用于地理空间索引的数据类型,或者一个用于时间序列数据的数据类型。
新的命令 (Commands): 模块可以注册新的 Redis 命令,这些命令可以操作模块自定义的数据类型,或者执行其他自定义逻辑。
事件处理 (Event Handlers): 模块可以注册事件处理器,监听 Redis 服务器内部发生的事件,例如键过期、键删除等,并根据事件做出相应的处理。
阻塞命令 (Blocking Commands): 模块可以实现阻塞命令,允许客户端等待某些条件满足后再返回结果,这对于实现消息队列、分布式锁等功能非常有用。
数据持久化 (Persistence): 模块可以自定义数据的持久化方式,确保模块数据的可靠性。
集群支持 (Cluster Support): 模块可以设计为在 Redis 集群环境中工作,实现分布式数据管理和处理。
3. Redis 模块 API:
Redis 提供了丰富的 C API,供模块开发者使用。这些 API 涵盖了数据类型操作、命令注册、事件处理、内存管理、日志记录等方面。
模块通过调用这些 API 与 Redis 服务器进行交互,实现自定义功能。
4. 模块的加载和卸载:
模块可以在 Redis 服务器启动时通过配置文件 redis.conf 中的 loadmodule 指令加载。
模块也可以在 Redis 运行时使用 MODULE LOAD 命令动态加载。
可以使用 MODULE UNLOAD 命令卸载已加载的模块。
要开始 Redis 模块的开发,我们需要搭建开发环境并了解模块的基本结构。
1. 开发环境搭建:
Redis 服务器: 你需要安装 Redis 4.0 或更高版本的服务器。
C 编译器: 例如 GCC 或 Clang。
Build 工具: 例如 make。
Redis 模块开发头文件: 通常包含在 Redis 服务器的源代码中,你需要找到 redismodule.h 头文件,并确保你的编译环境能够找到它。 一种常见的做法是将 Redis 源代码克隆到本地,并在模块编译时指定头文件路径。
2. 模块基本结构:
一个最简单的 Redis 模块 C 代码文件通常包含以下结构:
#include "redismodule.h" int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (RedisModule_Init(ctx, "mymodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } // 在这里注册你的命令、数据类型、事件处理器等 return REDISMODULE_OK; }
#include "redismodule.h": 包含 Redis 模块 API 头文件,这是编写模块的必要步骤。
RedisModule_OnLoad 函数: 这是模块的入口函数,在模块加载时被 Redis 服务器调用。
RedisModuleCtx *ctx: 模块上下文,提供了与 Redis 服务器交互的各种 API。
RedisModuleString **argv, int argc: 模块加载时传递的参数,类似于 MODULE LOAD 命令的参数。
函数返回值:REDISMODULE_OK 表示模块加载成功,REDISMODULE_ERR 表示加载失败。
RedisModule_Init 函数: 必须在 RedisModule_OnLoad 函数中调用,用于初始化模块。
ctx: 模块上下文。
"mymodule": 模块名称。
1: 模块版本号。
REDISMODULE_APIVER_1: Redis 模块 API 版本号,通常使用 REDISMODULE_APIVER_1。
我们来通过一个简单的例子,开发一个名为 hello 的 Redis 模块,它提供一个命令 hello.ping,该命令返回 "Hello, Redis Modules!"。
1. 创建 hello.c 文件:
#include "redismodule.h" /* * hello.ping 命令的实现 */ int HelloPingCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 1) { return RedisModule_WrongArity(ctx); // 参数数量错误 } RedisModule_ReplyWithString(ctx, RedisModule_CreateString(ctx, "Hello, Redis Modules!", strlen("Hello, Redis Modules!"))); return REDISMODULE_OK; } /* * 模块加载时调用的函数 */ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (RedisModule_Init(ctx, "hello", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } /* 注册 hello.ping 命令 */ if (RedisModule_CreateCommand(ctx, "hello.ping", HelloPingCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } return REDISMODULE_OK; }
HelloPingCommand 函数: 这是 hello.ping 命令的实现函数。
RedisModule_WrongArity(ctx): 用于处理命令参数数量错误的情况。
RedisModule_ReplyWithString(ctx, ...): 用于向客户端发送字符串类型的回复。
RedisModule_CreateString(ctx, "...", strlen("...")): 用于创建 Redis 字符串对象。
RedisModule_CreateCommand 函数: 用于注册新的 Redis 命令。
ctx: 模块上下文。
"hello.ping": 命令名称。
HelloPingCommand: 命令处理函数。
"readonly": 命令标志,表示该命令是只读的,不会修改数据。其他常用标志包括 write, deny-oom, admin, pubsub, noscript, loading, stale, fast.
1, 1, 1: 命令参数数量的范围,这里表示命令必须有 1 个参数(命令本身)。
2. 编译模块:
假设你已经安装了 GCC,并且 redismodule.h 头文件在 /path/to/redis/src 目录下。 你可以使用以下命令编译模块:
gcc -fPIC -shared hello.c -o hello.so -I /path/to/redis/src
-fPIC: 生成位置无关代码,用于动态链接库。
-shared: 生成动态链接库。
hello.c: 模块源代码文件。
-o hello.so: 输出文件名,通常以 .so 结尾。
-I /path/to/redis/src: 指定 Redis 头文件所在目录。 请根据你的实际情况修改路径。
3. 加载模块到 Redis:
启动 Redis 服务器,然后在 redis-cli 中执行以下命令加载模块:
MODULE LOAD ./hello.so
MODULE LOAD: 加载模块命令。
./hello.so: 模块文件路径,请根据你的实际情况修改。
4. 测试模块命令:
加载成功后,你就可以在 redis-cli 中使用 hello.ping 命令了:
127.0.0.1:6379> hello.ping "Hello, Redis Modules!"
如果看到 "Hello, Redis Modules!" 的回复,说明你的模块已经成功加载并运行了。
5. 卸载模块:
可以使用以下命令卸载模块:
MODULE UNLOAD hello
MODULE UNLOAD: 卸载模块命令。
hello: 模块名称 (在 RedisModule_Init 中指定的名称)。
Redis 模块最强大的功能之一是允许定义自定义数据类型。 我们可以创建一个模块,实现一个更复杂的数据结构,并提供相应的操作命令。 例如,我们可以创建一个简单的列表数据类型。
1. 定义数据类型结构体:
在 listmodule.c 文件中,我们首先定义列表数据类型的结构体:
typedef struct { RedisModuleString **items; // 存储列表元素的数组 size_t len; // 列表长度 size_t capacity; // 数组容量 } ListType;
2. 实现数据类型操作函数:
我们需要实现一些函数来操作 ListType 数据类型,例如创建、添加元素、获取元素、释放内存等。
// 创建一个新的 ListType 对象 ListType *ListType_Create() { ListType *list = RedisModule_Alloc(sizeof(ListType)); if (list == NULL) return NULL; list->items = NULL; list->len = 0; list->capacity = 0; return list; } // 向列表末尾添加元素 int ListType_Append(ListType *list, RedisModuleString *item) { if (list->len >= list->capacity) { size_t new_capacity = list->capacity == 0 ? 4 : list->capacity * 2; RedisModuleString **new_items = RedisModule_Realloc(list->items, sizeof(RedisModuleString*) * new_capacity); if (new_items == NULL) return REDISMODULE_ERR; list->items = new_items; list->capacity = new_capacity; } list->items[list->len++] = item; // 注意:这里没有复制字符串,而是直接使用了传入的 RedisModuleString 指针,需要注意内存管理 return REDISMODULE_OK; } // 获取列表指定索引的元素 RedisModuleString *ListType_Get(ListType *list, size_t index) { if (index >= list->len) return NULL; return list->items[index]; } // 获取列表长度 size_t ListType_Len(ListType *list) { return list->len; } // 释放 ListType 对象占用的内存 void ListType_Free(ListType *list) { if (list == NULL) return; for (size_t i = 0; i < list->len; ++i) { RedisModule_FreeString(NULL, list->items[i]); // 释放字符串 } RedisModule_Free(list->items); RedisModule_Free(list); }
3. 注册数据类型和相关命令:
在 RedisModule_OnLoad 函数中,我们需要注册自定义数据类型,并注册操作该数据类型的命令。
#include "redismodule.h" #include <stdlib.h> // for atoi // ... (ListType 结构体和操作函数定义,如上所示) ... // 数据类型描述符 RedisModuleType *ListModuleType; // list.create 命令实现 int ListCreateCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 1) return RedisModule_WrongArity(ctx); ListType *list = ListType_Create(); if (list == NULL) { return RedisModule_ReplyWithError(ctx, "OOM"); } // 将 ListType 对象关联到 Redis 键 RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY) { RedisModule_CloseKey(key); ListType_Free(list); return RedisModule_ReplyWithError(ctx, "Key already exists"); } RedisModule_ModuleTypeSetValue(key, ListModuleType, list); // 设置键的值为 ListType 对象 RedisModule_CloseKey(key); RedisModule_ReplyStatusOK(ctx); return REDISMODULE_OK; } // list.append 命令实现 int ListAppendCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 3) return RedisModule_WrongArity(ctx); RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "Key does not exist"); } ListType *list = RedisModule_ModuleTypeGetValue(key); // 获取键关联的 ListType 对象 if (list == NULL || RedisModule_ModuleTypeGetType(key) != ListModuleType) { // 检查类型 RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "WRONGTYPE Operation against a key holding the wrong kind of value"); } RedisModuleString *item = RedisModule_StringDup(ctx, argv[2]); // 复制字符串,避免外部修改 if (ListType_Append(list, item) == REDISMODULE_ERR) { RedisModule_CloseKey(key); RedisModule_FreeString(ctx, item); return RedisModule_ReplyWithError(ctx, "OOM"); } RedisModule_CloseKey(key); RedisModule_ReplyStatusOK(ctx); return REDISMODULE_OK; } // list.len 命令实现 int ListLenCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 2) return RedisModule_WrongArity(ctx); RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "Key does not exist"); } ListType *list = RedisModule_ModuleTypeGetValue(key); if (list == NULL || RedisModule_ModuleTypeGetType(key) != ListModuleType) { RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "WRONGTYPE Operation against a key holding the wrong kind of value"); } RedisModule_CloseKey(key); RedisModule_ReplyWithLongLong(ctx, ListType_Len(list)); return REDISMODULE_OK; } // list.get 命令实现 int ListGetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 3) return RedisModule_WrongArity(ctx); RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ); if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) { RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "Key does not exist"); } ListType *list = RedisModule_ModuleTypeGetValue(key); if (list == NULL || RedisModule_ModuleTypeGetType(key) != ListModuleType) { RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "WRONGTYPE Operation against a key holding the wrong kind of value"); } long long index_long; if (RedisModule_StringToLongLong(argv[2], &index_long) != REDISMODULE_OK || index_long < 0) { RedisModule_CloseKey(key); return RedisModule_ReplyWithError(ctx, "Invalid index"); } size_t index = (size_t)index_long; RedisModuleString *item = ListType_Get(list, index); RedisModule_CloseKey(key); if (item == NULL) { RedisModule_ReplyNull(ctx); // 索引越界,返回 NULL } else { RedisModule_ReplyWithString(ctx, item); // 返回元素 } return REDISMODULE_OK; } // 数据类型释放回调函数,在键被删除或模块卸载时调用 void ListType_RdbLoad(RedisModuleIO *io, int encver, RedisModuleType *type) { // ... (持久化加载逻辑,这里先留空) ... } void ListType_RdbSave(RedisModuleIO *io, RedisModuleType *type, void *value) { // ... (持久化保存逻辑,这里先留空) ... } void ListType_AofRewrite(RedisModuleIO *io, RedisModuleString *keyname, RedisModuleType *type, void *value) { // ... (AOF 重写逻辑,这里先留空) ... } void ListType_FreeCallback(RedisModuleType *type, void *value) { ListType_Free((ListType*)value); } int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (RedisModule_Init(ctx, "listmodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } // 注册自定义数据类型 RedisModuleTypeMethods listTypeMethods = { .version = REDISMODULE_TYPE_METHOD_VERSION, .rdb_load = ListType_RdbLoad, .rdb_save = ListType_RdbSave, .aof_rewrite = ListType_AofRewrite, .free_object = ListType_FreeCallback }; ListModuleType = RedisModule_CreateDataType(ctx, "mylist", 1, &listTypeMethods); if (ListModuleType == NULL) return REDISMODULE_ERR; // 注册命令 if (RedisModule_CreateCommand(ctx, "list.create", ListCreateCommand, "write", 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; if (RedisModule_CreateCommand(ctx, "list.append", ListAppendCommand, "write", 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; if (RedisModule_CreateCommand(ctx, "list.len", ListLenCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; if (RedisModule_CreateCommand(ctx, "list.get", ListGetCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) return REDISMODULE_ERR; return REDISMODULE_OK; }
RedisModuleType *ListModuleType;: 定义数据类型描述符的全局变量。
RedisModuleTypeMethods listTypeMethods: 定义数据类型方法结构体,包含 RDB 加载/保存、AOF 重写、释放回调函数等。
RedisModule_CreateDataType: 注册自定义数据类型。
ctx: 模块上下文。
"mylist": 数据类型名称,用于在 Redis 内部标识数据类型。
1: 数据类型版本号。
&listTypeMethods: 数据类型方法结构体指针。
RedisModule_ModuleTypeSetValue: 将自定义数据类型的值关联到 Redis 键。
RedisModule_ModuleTypeGetValue: 获取 Redis 键关联的自定义数据类型的值。
RedisModule_ModuleTypeGetType: 获取 Redis 键关联的值的数据类型。
ListType_RdbLoad, ListType_RdbSave, ListType_AofRewrite, ListType_FreeCallback: 数据类型的持久化和内存管理回调函数。 在示例代码中,持久化部分留空,实际应用中需要根据需求实现。
4. 编译和测试 listmodule:
编译 listmodule.c (编译命令类似 hello.so 的编译命令,只需将文件名替换为 listmodule.c 和 listmodule.so),然后加载到 Redis。
测试命令:
127.0.0.1:6379> list.create mylist OK 127.0.0.1:6379> list.append mylist item1 OK 127.0.0.1:6379> list.append mylist item2 OK 127.0.0.1:6379> list.len mylist (integer) 2 127.0.0.1:6379> list.get mylist 0 "item1" 127.0.0.1:6379> list.get mylist 1 "item2" 127.0.0.1:6379> list.get mylist 2 (nil)
除了自定义数据类型和命令,Redis 模块还支持更多高级特性,例如事件处理、阻塞命令、数据持久化等。
1. 事件处理:
模块可以注册事件处理器,监听 Redis 服务器内部发生的事件。 例如,监听 notify-keyspace-events 配置项启用的事件 (例如 keyspace 事件,keyevent 事件)。
// 事件处理回调函数 void MyModuleEventHandler(RedisModuleCtx *ctx, int event_type, uint64_t subevent, void *data) { switch (event_type) { case REDISMODULE_EVENT_KEYSPACE: // 处理 keyspace 事件 if (subevent == REDISMODULE_SUBEVENT_KEYSPACE_GENERIC) { // 通用 keyspace 事件,例如键过期、键删除等 RedisModuleEventGeneric *event_data = (RedisModuleEventGeneric*)data; RedisModule_Log(ctx, "notice", "Keyspace event: %s, key: %s", RedisModule_EventTypeName(event_type), RedisModule_StringPtrLen(event_data->key, NULL)); } break; case REDISMODULE_EVENT_CLIENT_CONN: // 处理客户端连接事件 if (subevent == REDISMODULE_SUBEVENT_CLIENT_CONNECTED) { RedisModuleClientInfo *client_info = (RedisModuleClientInfo*)data; RedisModule_Log(ctx, "notice", "Client connected: addr=%s, port=%d", client_info->ip, client_info->port); } break; // ... 其他事件类型 ... } } int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // ... (模块初始化) ... // 注册事件处理器 if (RedisModule_SubscribeToServerEvent(ctx, REDISMODULE_EVENT_KEYSPACE | REDISMODULE_EVENT_CLIENT_CONN, MyModuleEventHandler) == REDISMODULE_ERR) { return REDISMODULE_ERR; } // ... (注册命令等) ... return REDISMODULE_OK; }
RedisModule_SubscribeToServerEvent: 注册事件处理器。
ctx: 模块上下文。
REDISMODULE_EVENT_KEYSPACE | REDISMODULE_EVENT_CLIENT_CONN: 要监听的事件类型,可以使用位或运算符组合多个事件类型。
MyModuleEventHandler: 事件处理回调函数。
RedisModuleEventHandler: 事件处理回调函数的类型定义。
event_type: 事件类型。
subevent: 子事件类型。
data: 事件数据,根据事件类型不同,数据类型也不同。
2. 阻塞命令:
模块可以实现阻塞命令,允许客户端等待某些条件满足后再返回结果。 例如,可以实现一个简单的消息队列的 BLPOP 命令。
// 阻塞命令实现 int BlockingPopCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc != 2) return RedisModule_WrongArity(ctx); RedisModuleString *key_name = argv[1]; // 尝试从列表中获取元素 (假设 ListType 模块已实现) RedisModuleKey *key = RedisModule_OpenKey(ctx, key_name, REDISMODULE_READ); ListType *list = RedisModule_ModuleTypeGetValue(key); RedisModule_CloseKey(key); if (list != NULL && ListType_Len(list) > 0) { // 列表中有元素,立即返回 RedisModuleString *item = ListType_Get(list, 0); // 假设获取第一个元素 // ... (从列表中移除元素,此处省略) ... RedisModule_ReplyWithString(ctx, item); return REDISMODULE_OK; } else { // 列表为空,进入阻塞状态 RedisModule_BlockClient(ctx, BlockingPopCommand, NULL, NULL, 0); // 阻塞客户端 // 当其他客户端向列表中添加元素时,会唤醒阻塞的客户端,并重新执行 BlockingPopCommand return REDISMODULE_ERR; // 返回 REDISMODULE_ERR 表示命令尚未完成,等待唤醒 } } int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { // ... (模块初始化) ... // 注册阻塞命令 if (RedisModule_CreateCommand(ctx, "list.bpop", BlockingPopCommand, "block|readonly", 1, 1, 1) == REDISMODULE_ERR) { return REDISMODULE_ERR; } // ... (注册其他命令等) ... return REDISMODULE_OK; }
RedisModule_BlockClient: 阻塞客户端。
ctx: 模块上下文。
BlockingPopCommand: 阻塞命令的处理函数,当条件满足时,Redis 服务器会重新调用该函数。
NULL, NULL, 0: 阻塞命令的参数,这里暂时使用 NULL。
3. 数据持久化:
模块需要实现数据类型的 RDB 和 AOF 持久化逻辑,确保模块数据的可靠性。 在 listmodule.c 的示例代码中,ListType_RdbLoad, ListType_RdbSave, ListType_AofRewrite 函数留空,实际应用中需要根据数据类型和持久化需求进行实现。
RDB 持久化: ListType_RdbSave 函数负责将数据类型的值写入 RDB 文件,ListType_RdbLoad 函数负责从 RDB 文件加载数据类型的值。
AOF 持久化: ListType_AofRewrite 函数负责生成 AOF 重写命令,将数据类型的值转换为 AOF 命令进行持久化。
Redis 模块极大地扩展了 Redis 的应用场景,以下是一些典型的应用场景: