好的,各位观众,欢迎来到今天的Redis Modules API讲座!今天我们不搞虚头巴脑的,直接上干货,手把手带大家玩转Redis Modules API,让你的Redis也能像瑞士军刀一样,想干啥就干啥!
第一部分:Redis Modules API 是个啥?
首先,咱得搞清楚Redis Modules API是干嘛的。简单来说,就是Redis官方提供的一套C/C++接口,允许你用C/C++编写自定义的功能模块,然后加载到Redis服务器中,扩展Redis的能力。
想象一下,Redis原本只能存字符串、列表、集合、哈希表这些基本数据结构,如果你想存个更复杂的数据结构,比如树、图,或者想实现一些特殊的算法,比如图像处理、机器学习,怎么办?用Modules API啊!
这就像给Redis装了个插件,让它从一台普通的数据库,变成了一台拥有无限可能的超级数据库。
第二部分:为什么要用 Redis Modules API?
你可能会问,我用Redis不挺好的吗?为什么要费劲巴拉地写C/C++模块?
原因很简单:
- 性能!性能!还是性能! C/C++是系统级语言,性能比脚本语言(如Lua)高得多。对于计算密集型的任务,用C/C++模块可以显著提高性能。
- 扩展性! Redis内置的数据结构和命令是有限的,Modules API允许你自定义数据结构和命令,满足各种奇奇怪怪的需求。
- 与其他库的集成! C/C++可以方便地调用其他第三方库,比如图像处理库、机器学习库,让Redis具备更强大的能力。
- 代码重用! 你可以将一些通用的功能封装成模块,供多个Redis实例使用,提高代码重用率。
总而言之,Modules API就是Redis的“外挂”,让你能突破Redis本身的限制,打造更强大的Redis应用。
第三部分:Hello, World! 第一个Redis Module
废话不多说,咱们直接上手写一个最简单的Redis Module,功能就是定义一个新的命令hello.world
,调用后返回字符串 "Hello, Redis Module!"
。
- 创建源文件
hello.c
:
#include <redismodule.h>
int HelloWorldCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_CHECK_ARITY(argc, 1, 1); // 检查参数个数,必须是1个
RedisModule_ReplyWithString(ctx, RedisModule_CreateString(ctx, "Hello, Redis Module!", strlen("Hello, Redis Module!")));
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;
}
if (RedisModule_CreateCommand(ctx, "hello.world", HelloWorldCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
-
编译源文件:
你需要先安装Redis的开发包,然后用以下命令编译:
gcc -fPIC -shared hello.c -o hello.so -I/path/to/redis/src
注意把 /path/to/redis/src
替换成你的Redis源码目录。如果你是从Redis官网下载的源码,解压后就能找到 src
目录。
-
加载模块:
在Redis的配置文件
redis.conf
中添加一行:
loadmodule /path/to/hello.so
把 /path/to/hello.so
替换成你编译生成的 hello.so
文件的路径。或者,你也可以在Redis客户端中使用 MODULE LOAD
命令加载:
redis-cli MODULE LOAD /path/to/hello.so
-
测试:
重启Redis服务器,然后在Redis客户端中执行命令:
127.0.0.1:6379> hello.world
"Hello, Redis Module!"
看到这个结果,恭喜你,你的第一个Redis Module就成功了!
代码解析:
#include <redismodule.h>
: 引入Redis Modules API的头文件。RedisModule_OnLoad()
: 这是模块的入口函数,当模块被加载时,Redis会调用这个函数。RedisModule_Init()
: 初始化模块,指定模块名、版本号和API版本。RedisModule_CreateCommand()
: 创建一个新的命令,指定命令名、处理函数、标志位等。HelloWorldCommand()
: 这是命令的处理函数,当用户执行hello.world
命令时,Redis会调用这个函数。RedisModule_ReplyWithString()
: 向客户端发送字符串回复。RedisModule_CreateString()
: 创建一个Redis字符串对象。REDISMODULE_CHECK_ARITY()
: 检查命令参数的个数。
第四部分:Modules API 常用函数详解
上面只是一个简单的例子,Modules API的功能远不止于此。下面我们来详细介绍一些常用的API函数:
1. 字符串操作:
RedisModule_CreateString(RedisModuleCtx *ctx, const char *str, size_t len)
: 创建一个Redis字符串对象。RedisModule_CreateStringFromString(RedisModuleCtx *ctx, RedisModuleString *s)
: 从另一个Redis字符串对象创建一个新的字符串对象。RedisModule_StringPtrLen(RedisModuleString *s, size_t *len)
: 获取Redis字符串对象的指针和长度。RedisModule_FreeString(RedisModuleCtx *ctx, RedisModuleString *s)
: 释放Redis字符串对象。
2. 数据类型操作:
RedisModule_SetValue(RedisModuleCtx *ctx, RedisModuleString *key, int type, void *value)
: 设置一个键的值,指定数据类型和值。RedisModule_GetValue(RedisModuleCtx *ctx, RedisModuleString *key, int *type)
: 获取一个键的值,返回数据类型。RedisModule_DeleteKey(RedisModuleCtx *ctx, RedisModuleString *key)
: 删除一个键。
3. 回复客户端:
RedisModule_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *s)
: 向客户端发送字符串回复。RedisModule_ReplyWithLongLong(RedisModuleCtx *ctx, long long ll)
: 向客户端发送整数回复。RedisModule_ReplyWithDouble(RedisModuleCtx *ctx, double d)
: 向客户端发送浮点数回复。RedisModule_ReplyWithError(RedisModuleCtx *ctx, const char *err)
: 向客户端发送错误回复。RedisModule_ReplyWithArray(RedisModuleCtx *ctx, long len)
: 向客户端发送数组回复,需要先调用这个函数,然后逐个添加数组元素。RedisModule_ReplyWithNull(RedisModuleCtx *ctx)
: 向客户端发送空回复。
4. 其他常用函数:
RedisModule_Log(RedisModuleCtx *ctx, const char *level, const char *fmt, ...)
: 记录日志,level
可以是"debug"
,"verbose"
,"notice"
,"warning"
。RedisModule_GetContextFlags(RedisModuleCtx *ctx)
: 获取上下文标志,可以用来判断当前是否在读写复制流中。RedisModule_Call(RedisModuleCtx *ctx, const char *cmd, const char *fmt, ...)
: 调用Redis内置命令。
表格:常用Redis Modules API 函数总结
函数名 | 功能 |
---|---|
RedisModule_CreateString() |
创建一个Redis字符串对象 |
RedisModule_StringPtrLen() |
获取Redis字符串对象的指针和长度 |
RedisModule_SetValue() |
设置一个键的值,指定数据类型和值 |
RedisModule_GetValue() |
获取一个键的值,返回数据类型 |
RedisModule_DeleteKey() |
删除一个键 |
RedisModule_ReplyWithString() |
向客户端发送字符串回复 |
RedisModule_ReplyWithLongLong() |
向客户端发送整数回复 |
RedisModule_ReplyWithDouble() |
向客户端发送浮点数回复 |
RedisModule_ReplyWithError() |
向客户端发送错误回复 |
RedisModule_ReplyWithArray() |
向客户端发送数组回复 |
RedisModule_ReplyWithNull() |
向客户端发送空回复 |
RedisModule_Log() |
记录日志 |
RedisModule_Call() |
调用Redis内置命令 |
REDISMODULE_CHECK_ARITY() |
检查命令参数的个数 |
第五部分:实战演练:一个简单的计数器模块
现在,让我们来写一个稍微复杂一点的Redis Module,实现一个简单的计数器功能。我们需要定义两个命令:
counter.incr <key>
: 将指定键的计数器加1,如果键不存在,则创建并初始化为0。counter.get <key>
: 获取指定键的计数器的值。
#include <redismodule.h>
#include <stdio.h>
int CounterIncrCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_CHECK_ARITY(argc, 2, 2);
RedisModuleString *key = argv[1];
long long value;
int type = REDISMODULE_T_STRING; // 默认类型是字符串
// 尝试获取键的值
if (RedisModule_GetValue(ctx, key, &type) == REDISMODULE_ERR) {
value = 0; // 键不存在,初始化为0
} else {
// 键存在,判断类型是否为字符串
if (type != REDISMODULE_T_STRING) {
RedisModule_ReplyWithError(ctx, "Key exists but is not a string");
return REDISMODULE_ERR;
}
// 获取字符串的值,并转换为整数
size_t len;
const char *str = RedisModule_StringPtrLen(RedisModule_GetValue(ctx, key, &type), &len);
if (str == NULL) {
value = 0;
} else {
if (sscanf(str, "%lld", &value) != 1) {
RedisModule_ReplyWithError(ctx, "String value is not a valid integer");
return REDISMODULE_ERR;
}
}
}
// 计数器加1
value++;
// 将新的值转换为字符串
char buf[32];
snprintf(buf, sizeof(buf), "%lld", value);
RedisModuleString *newValue = RedisModule_CreateString(ctx, buf, strlen(buf));
// 设置键的值
if (RedisModule_SetValue(ctx, key, REDISMODULE_T_STRING, newValue) == REDISMODULE_ERR) {
RedisModule_ReplyWithError(ctx, "Failed to set value");
RedisModule_FreeString(ctx, newValue); // 释放字符串
return REDISMODULE_ERR;
}
RedisModule_ReplyWithLongLong(ctx, value); // 返回新的值
RedisModule_FreeString(ctx, newValue); // 释放字符串
return REDISMODULE_OK;
}
int CounterGetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_CHECK_ARITY(argc, 2, 2);
RedisModuleString *key = argv[1];
long long value;
int type = REDISMODULE_T_STRING;
// 尝试获取键的值
if (RedisModule_GetValue(ctx, key, &type) == REDISMODULE_ERR) {
value = 0; // 键不存在,返回0
} else {
// 键存在,判断类型是否为字符串
if (type != REDISMODULE_T_STRING) {
RedisModule_ReplyWithError(ctx, "Key exists but is not a string");
return REDISMODULE_ERR;
}
size_t len;
const char *str = RedisModule_StringPtrLen(RedisModule_GetValue(ctx, key, &type), &len);
if (str == NULL) {
value = 0;
} else {
if (sscanf(str, "%lld", &value) != 1) {
RedisModule_ReplyWithError(ctx, "String value is not a valid integer");
return REDISMODULE_ERR;
}
}
}
RedisModule_ReplyWithLongLong(ctx, value); // 返回计数器的值
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx, "counter", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
if (RedisModule_CreateCommand(ctx, "counter.incr", CounterIncrCommand, "write", 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
if (RedisModule_CreateCommand(ctx, "counter.get", CounterGetCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
代码解析:
CounterIncrCommand()
: 实现计数器加1的功能。首先尝试获取键的值,如果键不存在,则初始化为0。然后将计数器加1,并将新的值设置回键中。CounterGetCommand()
: 实现获取计数器值的功能。如果键不存在,则返回0。REDISMODULE_T_STRING
: 表示数据类型为字符串。"write"
和"readonly"
: 这是命令的标志位,"write"
表示命令会修改数据,"readonly"
表示命令只读取数据。
编译和加载:
按照之前的步骤,编译并加载这个模块。
测试:
127.0.0.1:6379> counter.incr mycounter
(integer) 1
127.0.0.1:6379> counter.incr mycounter
(integer) 2
127.0.0.1:6379> counter.get mycounter
(integer) 2
127.0.0.1:6379> counter.get anothercounter
(integer) 0
第六部分:最佳实践和注意事项
- 错误处理! 在Modules API中,错误处理非常重要。一定要检查函数的返回值,并根据错误码进行相应的处理。
- 内存管理! Redis Modules API使用手动内存管理。你需要负责分配和释放内存,避免内存泄漏。
RedisModule_FreeString()
一定要及时调用。 - 线程安全! Redis是单线程的,但是Modules API可以在后台线程中执行任务。如果你的模块需要在后台线程中访问Redis数据,需要注意线程安全问题。可以使用 Redis提供的锁机制,例如
RedisModule_LockKey
和RedisModule_UnlockKey
。 - 数据类型选择! 根据实际需求选择合适的数据类型。如果需要存储复杂的数据结构,可以考虑使用Redis的list、set、hash等数据结构,或者自定义数据结构。
- 避免阻塞! 尽量避免在命令处理函数中执行耗时的操作,以免阻塞Redis服务器。可以将耗时的操作放到后台线程中执行。
- 使用Redis提供的工具! Redis提供了一些工具,可以帮助你开发和调试Modules,比如
redis-cli --eval
可以用来执行Lua脚本,redis-server --module-config
可以用来配置模块参数。 - 命名冲突! 创建的命令最好带上模块名前缀,例如
mymodule.command
,以避免与其他模块或Redis内置命令冲突。
第七部分:进阶技巧
- 自定义数据类型: Modules API允许你定义自己的数据类型,例如树、图等。这需要你实现一些额外的函数,比如数据类型的序列化和反序列化函数。
- 阻塞命令: 你可以创建阻塞命令,让客户端等待某个条件满足后再返回结果。这需要使用Redis的阻塞队列和信号量。
- 事件通知: Modules API允许你注册事件通知,当Redis发生某些事件时,你的模块会收到通知。例如,你可以注册键过期事件,当某个键过期时,你的模块会收到通知。
第八部分:总结
Redis Modules API是一个强大的工具,可以让你扩展Redis的功能,满足各种奇奇怪怪的需求。虽然学习曲线可能有点陡峭,但是一旦掌握了,你就能打造出更强大的Redis应用。
希望今天的讲座能帮助大家入门Redis Modules API。记住,实践是检验真理的唯一标准!赶紧动手试试吧!
祝大家编程愉快!