Redis4.0版本后开始支持自定义module,通过module可以扩展Redis,实现自定义命令。RediSearch、RedisJSON等这些都是通过module实现的。
更多Redis module可以查看官网文档:Redis
自定义module
module开发
module可以通过C语言开发,module需要实现RedisModule_OnLoad函数,在onload函数中使用
RedisModule_CreateCommand创建Redis命令,获取pid代码示例:
#include "redismodule.h"
#include <unistd.h>
int GetPid_Command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
RedisModule_ReplyWithLongLong(ctx, getpid());
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;
if (RedisModule_CreateCommand(ctx,"getpid",
GetPid_Command, "readonly",
0, 0, 0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
更多实例代码详见:https://github.com/ffantong/myredis
编译
执行gcc编译命令,编译成动态链接库
gcc getpid.c -fPIC -shared -o libgetpid.so -I ${REDIS_HOME}/src
加载module
Redis支持动态加载module也可以通过配置文件配置
配置文件加载
# Load modules at startup. If the server is not able to load modules
# it will abort. It is possible to use multiple loadmodule directives.
#
loadmodule /home/fengxw/libmymodule.so
# loadmodule /path/to/other_module.so
动态加载
可以通过module load 和module unload命令实现module的加载和卸载
127.0.0.1:6379> module list
1) 1) "name"
2) "helloworld"
3) "ver"
4) (integer) 1
127.0.0.1:6379>
127.0.0.1:6379> module unload helloworld
OK
127.0.0.1:6379> module list
(empty array)
127.0.0.1:6379>
127.0.0.1:6379> module load /home/fengxw/libgetpid.so
OK
127.0.0.1:6379> GETPID
(integer) 17338
127.0.0.1:6379>
module api
module api可以查看官方文档:Redis Modules: an introduction to the API – Redis
module实现原理
加载module
通过dlopen函数加载链接库到应用
/* Load a module and initialize it. On success C_OK is returned, otherwise
* C_ERR is returned. */
int moduleLoad(const char *path, void **module_argv, int module_argc) {
int (*onload)(void *, void **, int);
void *handle;
RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
ctx.client = moduleFreeContextReusedClient;
selectDb(ctx.client, 0);
struct stat st;
if (stat(path, &st) == 0)
{ // this check is best effort
if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
serverLog(LL_WARNING, "Module %s failed to load: It does not have execute permissions.", path);
return C_ERR;
}
}
handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
if (handle == NULL) {
serverLog(LL_WARNING, "Module %s failed to load: %s", path, dlerror());
return C_ERR;
}
onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
if (onload == NULL) {
dlclose(handle);
serverLog(LL_WARNING,
"Module %s does not export RedisModule_OnLoad() "
"symbol. Module not loaded.",path);
return C_ERR;
}
if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {
if (ctx.module) {
moduleUnregisterCommands(ctx.module);
moduleUnregisterSharedAPI(ctx.module);
moduleUnregisterUsedAPI(ctx.module);
moduleFreeModuleStructure(ctx.module);
}
dlclose(handle);
serverLog(LL_WARNING,
"Module %s initialization failed. Module not loaded",path);
return C_ERR;
}
/* Redis module loaded! Register it. */
dictAdd(modules,ctx.module->name,ctx.module);
ctx.module->blocked_clients = 0;
ctx.module->handle = handle;
serverLog(LL_NOTICE,"Module '%s' loaded from %s",ctx.module->name,path);
/* Fire the loaded modules event. */
moduleFireServerEvent(REDISMODULE_EVENT_MODULE_CHANGE,
REDISMODULE_SUBEVENT_MODULE_LOADED,
ctx.module);
moduleFreeContext(&ctx);
return C_OK;
}
卸载module
卸载模块注册的命令和一些内容,然后调用dlcolse卸载链接库
/* Unload the module registered with the specified name. On success
* C_OK is returned, otherwise C_ERR is returned and errno is set
* to the following values depending on the type of error:
*
* * ENONET: No such module having the specified name.
* * EBUSY: The module exports a new data type and can only be reloaded. */
int moduleUnload(sds name) {
struct RedisModule *module = dictFetchValue(modules,name);
if (module == NULL) {
errno = ENOENT;
return REDISMODULE_ERR;
} else if (listLength(module->types)) {
errno = EBUSY;
return REDISMODULE_ERR;
} else if (listLength(module->usedby)) {
errno = EPERM;
return REDISMODULE_ERR;
} else if (module->blocked_clients) {
errno = EAGAIN;
return REDISMODULE_ERR;
}
/* Give module a chance to clean up. */
int (*onunload)(void *);
onunload = (int (*)(void *))(unsigned long) dlsym(module->handle, "RedisModule_OnUnload");
if (onunload) {
RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
ctx.module = module;
ctx.client = moduleFreeContextReusedClient;
int unload_status = onunload((void*)&ctx);
moduleFreeContext(&ctx);
if (unload_status == REDISMODULE_ERR) {
serverLog(LL_WARNING, "Module %s OnUnload failed. Unload canceled.", name);
errno = ECANCELED;
return REDISMODULE_ERR;
}
}
moduleFreeAuthenticatedClients(module);
moduleUnregisterCommands(module);
moduleUnregisterSharedAPI(module);
moduleUnregisterUsedAPI(module);
moduleUnregisterFilters(module);
/* Remove any notification subscribers this module might have */
moduleUnsubscribeNotifications(module);
moduleUnsubscribeAllServerEvents(module);
/* Unload the dynamic library. */
if (dlclose(module->handle) == -1) {
char *error = dlerror();
if (error == NULL) error = "Unknown error";
serverLog(LL_WARNING,"Error when trying to close the %s module: %s",
module->name, error);
}
/* Fire the unloaded modules event. */
moduleFireServerEvent(REDISMODULE_EVENT_MODULE_CHANGE,
REDISMODULE_SUBEVENT_MODULE_UNLOADED,
module);
/* Remove from list of modules. */
serverLog(LL_NOTICE,"Module %s unloaded",module->name);
dictDelete(modules,module->name);
module->name = NULL; /* The name was already freed by dictDelete(). */
moduleFreeModuleStructure(module);
return REDISMODULE_OK;
}
命令注册
Redis有一个全局属性commands,保存Redis支持的全部命令,module校验通过后,会把命令加入到commands结构中
/* Register a new command in the Redis server, that will be handled by
* calling the function pointer 'func' using the RedisModule calling
* convention. The function returns REDISMODULE_ERR if the specified command
* name is already busy or a set of invalid flags were passed, otherwise
* REDISMODULE_OK is returned and the new command is registered.
*
* This function must be called during the initialization of the module
* inside the RedisModule_OnLoad() function. Calling this function outside
* of the initialization function is not defined.
*
* The command function type is the following:
*
* int MyCommand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
*
* And is supposed to always return REDISMODULE_OK.
*
* The set of flags 'strflags' specify the behavior of the command, and should
* be passed as a C string composed of space separated words, like for
* example "write deny-oom". The set of flags are:
*
* * **"write"**: The command may modify the data set (it may also read
* from it).
* * **"readonly"**: The command returns data from keys but never writes.
* * **"admin"**: The command is an administrative command (may change
* replication or perform similar tasks).
* * **"deny-oom"**: The command may use additional memory and should be
* denied during out of memory conditions.
* * **"deny-script"**: Don't allow this command in Lua scripts.
* * **"allow-loading"**: Allow this command while the server is loading data.
* Only commands not interacting with the data set
* should be allowed to run in this mode. If not sure
* don't use this flag.
* * **"pubsub"**: The command publishes things on Pub/Sub channels.
* * **"random"**: The command may have different outputs even starting
* from the same input arguments and key values.
* * **"allow-stale"**: The command is allowed to run on slaves that don't
* serve stale data. Don't use if you don't know what
* this means.
* * **"no-monitor"**: Don't propagate the command on monitor. Use this if
* the command has sensible data among the arguments.
* * **"no-slowlog"**: Don't log this command in the slowlog. Use this if
* the command has sensible data among the arguments.
* * **"fast"**: The command time complexity is not greater
* than O(log(N)) where N is the size of the collection or
* anything else representing the normal scalability
* issue with the command.
* * **"getkeys-api"**: The command implements the interface to return
* the arguments that are keys. Used when start/stop/step
* is not enough because of the command syntax.
* * **"no-cluster"**: The command should not register in Redis Cluster
* since is not designed to work with it because, for
* example, is unable to report the position of the
* keys, programmatically creates key names, or any
* other reason.
* * **"no-auth"**: This command can be run by an un-authenticated client.
* Normally this is used by a command that is used
* to authenticate a client.
* * **"may-replicate"**: This command may generate replication traffic, even
* though it's not a write command.
*/
int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) {
int64_t flags = strflags ? commandFlagsFromString((char*)strflags) : 0;
if (flags == -1) return REDISMODULE_ERR;
if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled)
return REDISMODULE_ERR;
struct redisCommand *rediscmd;
RedisModuleCommandProxy *cp;
sds cmdname = sdsnew(name);
/* Check if the command name is busy. */
if (lookupCommand(cmdname) != NULL) {
sdsfree(cmdname);
return REDISMODULE_ERR;
}
/* Create a command "proxy", which is a structure that is referenced
* in the command table, so that the generic command that works as
* binding between modules and Redis, can know what function to call
* and what the module is.
*
* Note that we use the Redis command table 'getkeys_proc' in order to
* pass a reference to the command proxy structure. */
cp = zmalloc(sizeof(*cp));
cp->module = ctx->module;
cp->func = cmdfunc;
cp->rediscmd = zmalloc(sizeof(*rediscmd));
cp->rediscmd->name = cmdname;
cp->rediscmd->proc = RedisModuleCommandDispatcher;
cp->rediscmd->arity = -1;
cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->getkeys_proc = (redisGetKeysProc*)(unsigned long)cp;
cp->rediscmd->firstkey = firstkey;
cp->rediscmd->lastkey = lastkey;
cp->rediscmd->keystep = keystep;
cp->rediscmd->microseconds = 0;
cp->rediscmd->calls = 0;
cp->rediscmd->rejected_calls = 0;
cp->rediscmd->failed_calls = 0;
dictAdd(server.commands,sdsdup(cmdname),cp->rediscmd);
dictAdd(server.orig_commands,sdsdup(cmdname),cp->rediscmd);
cp->rediscmd->id = ACLGetCommandID(cmdname); /* ID used for ACL. */
return REDISMODULE_OK;
}
命令执行
module执行和Redis内置命令执行没有特别大的区别,需要注意的是如果集群模式下,module方法是不支持跳转到对应的机器上。
//server.c - processCommand方法
int processCommand(client *c) {
//查找命令
/* Now lookup the command and check ASAP about trivial error conditions
* such as wrong arity, bad command name and so forth. */
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
//省略部分处理逻辑...
//集群跳转逻辑
/* If cluster is enabled perform the cluster redirection here.
* However we don't perform the redirection if:
* 1) The sender of this command is our master.
* 2) The command has no key arguments. */
if (server.cluster_enabled &&
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(!cmdHasMovableKeys(c->cmd) && c->cmd->firstkey == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
int error_code;
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,
&hashslot,&error_code);
if (n == NULL || n != server.cluster->myself) {
if (c->cmd->proc == execCommand) {
discardTransaction(c);
} else {
flagTransaction(c);
}
clusterRedirectClient(c,n,hashslot,error_code);
c->cmd->rejected_calls++;
return C_OK;
}
}
//执行命令处理逻辑 。。。
return C_OK;
}
/* Returns 1 for commands that may have key names in their arguments, but have
* no pre-determined key positions. */
static int cmdHasMovableKeys(struct redisCommand *cmd) {
return (cmd->getkeys_proc && !(cmd->flags & CMD_MODULE)) ||
cmd->flags & CMD_MODULE_GETKEYS;
}
总结
Redis module提供了非常强大的扩展能力,借助module可以扩展Redis的能力,同时module可以访问和修改Redis内部数据。但个人认为module也不适合做特别复杂的计算,由于Redis单线程特性,同时module的稳定性也会影响到Redis服务。