Redis4.0模塊子系統實現簡述


一、模塊加載方法

1、在配置文件或者啟動參數里面通過<loadmodule /path/to/mymodule.so args>指令加載

2、Redis啟動后,通過<module load /path/to/mymodule.so args>指令加載,另外<module list>可以查詢當前所有已加載模塊。<module unload name>可以卸載已經加載的模塊,注意name為模塊的注冊名字,不一定和模塊文件名相同。


二、介紹

Redis模塊是一種動態庫,可以用與Redis內核相似的運行速度和特性來擴展Redis內核的功能。作者認為lua腳本只是組合Redis內核的現有功能,但是Redis模塊則可以擴展Redis內核的功能。主要提供以下幾個方面的擴展

1、可以如lua腳本或者client一樣,通過RedisModule_Call接口直接執行redis命令並獲取執行結果。Redis稱呼這種API為高層API。

2、可以通過RedisModule_OpenKey接口,獲取底層鍵,並根據鍵的類型以及各類型提供的模塊操作接口進行底層操作。

3、自動內存管理(Automatic memory management),可以在回調函數中,調用RedisModule_AutoMemory打開自動內存管理功能,這樣隨后分配的RedisModuleString對象、open key等,redis會記錄下來,當回調函數返回的時候,redis會把這些資源自動釋放調。這意味着不能在自動內存管理打開的情況下,創建RedisModuleString等對象來初始化全局變量。

4、redis本地類型(native types support)創建。通過提供RDB保存、RDB加載、AOF重寫等回調函數,在Redis模塊中可以創建類似redis內部dict、list之類的數據類型。例如可以在模塊中創建一個鏈表,並提供對應的回調函數,這樣redis在保存RDB文件的時候,就可以把模塊中的數據保存在RDB中,在redis啟動從rdb中加載數據的時候,進而可以恢復模塊數據狀態。

5、阻塞命令。在redis模塊中可以將client阻塞,並設置超時時間。以實現類似BLPOP的阻塞命令。


三、一個redis模塊示例

如下代碼一個簡單的redis模塊示例,添加了一個hello.rand命令。在模塊加載的時候,打印出傳入的參數,當執行hello.rand命令的時候,同樣會打印出傳入的命令參數,並返回生成的一個隨機數。關於下面的代碼,有兩個點需要說明

1、RedisModule_OnLoad是每個Redis模塊的入口函數,在加載模塊的時候,就是通過查找這個函數的入口地址來開始執行redis模塊代碼的。

2、RedisModule_Init是在調用redis模塊API之前必須調用的初始化函數。一般應放在RedisModule_OnLoad的最開始位置。如果沒有執行RedisModule_Init,就調用redis模塊的API,則會產生空指針異常。

后面介紹redis實現的時候會進一步介紹上面的兩點

 
 
 
         
  1. #include "../../src/redismodule.h"
  2. #include <stdlib.h>
  3. #include <string.h>
  4. void HelloRedis_LogArgs(RedisModuleString **argv, int argc)
  5. {
  6.    for (int j = 0; j < argc; j++) {
  7.        const char *s = RedisModule_StringPtrLen(argv[j],NULL);
  8.        printf("ARGV[%d] = %s\n", j, s);
  9.    }
  10. }
  11. int HelloRedis_RandCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  12.    
  13.    HelloRedis_LogArgs(argv,argc);
  14.    RedisModule_ReplyWithLongLong(ctx,rand());
  15.    return REDISMODULE_OK;
  16. }
  17. int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  18.    
  19.    if (RedisModule_Init(ctx,"hello",1,REDISMODULE_APIVER_1)
  20.        == REDISMODULE_ERR) return REDISMODULE_ERR;
  21.    HelloRedis_LogArgs(argv,argc);
  22.    
  23.    if (RedisModule_CreateCommand(ctx,"hello.rand",
  24.        HelloRedis_RandCommand,"readonly",0,0,0)== REDISMODULE_ERR)
  25.        return REDISMODULE_ERR;
  26.        
  27.    
  28.    return REDISMODULE_OK;
  29. }

上面的模塊編譯執行后,client側執行如下命令來進行測試。

 
 
 
         
  1. 127.0.0.1:6379> module load modules/hellomodule/helloRedis.so helloarg1 helloarg2
  2. OK
  3. 127.0.0.1:6379> module list
  4. 1) 1) "name"
  5.   2) "hello"
  6.   3) "ver"
  7.   4) (integer) 1
  8. 127.0.0.1:6379> hello.rand
  9. (integer) 1315916238
  10. 127.0.0.1:6379> hello.rand
  11. (integer) 1420937835
  12. 127.0.0.1:6379> hello.rand arg test
  13. (integer) 543546598
  14. 127.0.0.1:6379> module unload hello
  15. OK

redis server端顯示的如下內容。

 
 
 
         
  1. ARGV[0] = helloarg1
  2. ARGV[1] = helloarg2
  3. 7779:M 19 Dec 14:33:17.032 * Module 'hello' loaded from modules/hellomodule/helloRedis.so
  4. ARGV[0] = hello.rand
  5. ARGV[0] = hello.rand
  6. ARGV[0] = hello.rand
  7. ARGV[1] = arg
  8. ARGV[2] = test
  9. 7779:M 19 Dec 14:34:13.604 * Module hello unloaded

四、redis模塊管理相關數據結構

Redis模塊管理涉及到的相關數據結構如下

 
 
 
         
  1. struct RedisModule {
  2.    void *handle;   /* dlopen() 返回的handle. */
  3.    char *name;     /* 模塊名字 */
  4.    int ver;        /* 模塊版本*/
  5.    int apiver;     /* 模塊API版本*/
  6.    list *types;    /* 用來保存模塊的數據類型信息 */
  7. };
  8. typedef struct RedisModule RedisModule;
  9. static dict *modules; /* 全局變量  用來進行module_name(SDS) -> RedisModule ptr的hash查找*/
  10. struct moduleLoadQueueEntry {
  11.    sds path;
  12.    int argc;
  13.    robj **argv;
  14. };
  15. struct redisServer {
  16.    ....
  17.    list *loadmodule_queue;     //在redis啟動的時候,用來保存命令行或者配置文件中的模塊相關配置,每個節點是一個struct moduleLoadQueueEntry
  18.    dict *moduleapi;            /* 導出的模塊API名字與API地址的映射 后面介紹*/
  19.    ....
  20. };
  21. struct redisServer server;
  22. static list *moduleUnblockedClients;    //當模塊中阻塞的client被RedisModule_UnblockClient接口解除阻塞的時候,會放入這個鏈表,后面統一處理

其中有幾個需要額外說明一下

1、RedisModule中的types成員用來保存Redis模塊中定義的native types,每個數據類型對應一個節點。每個節點的類型為struct RedisModuleType,里面包含了rdb_load、rdb_save、aof_rewrite等回調函數,這里沒有給出struct RedisModuleType。

2、server.loadmodule_queue這個隊列里面保存了redis通過命令行或者配置文件傳入的模塊加載信息,每個節點類型為struct moduleLoadQueueEntry。如配置文件指定"module load /path/to/mymodule.so arg1 arg2",則會構建一個struct moduleLoadQueueEntry,其中path成員為包含/path/to/mymodule.so的SDS,argc=2,argv則包含兩個robj對象指針,robj對象分別包含着"arg1"和"arg2"。

為什么沒有在加載配置的時候,直接加載模塊,而是先保存到隊列中呢?原因是在加載配置的時候,redis server還沒有完成初始化,加載模塊的時候,會調用模塊中的RedisModule_OnLoad函數,如果此時模塊訪問Redis內部數據,那么可能會訪問到無效的數據。因此需要加載的模塊需要先保存在隊列中,等redis初始化完畢后,在從隊列中依次加載對應的模塊。

3、關於moduleUnblockedClients,當模塊調用RedisModule_UnblockClient的時候,會先把要解除阻塞的client加入到這個鏈表中,等待當前redis的文件事件和時間事件處理完畢后,等待下一次事件前(beforeSleep->moduleHandleBlockedClients),來集中處理(例如調用模塊注冊的reply_callback函數等)。

這里為什么沒有直接在RedisModule_UnblockClient中處理,而是先添加到一個鏈表中,后面由redis內核處理呢?原因是RedisModule_UnblockClient在模塊中支持線程調用,而redis內核事件處理是單線程的,因此為了避免線程競爭會先把待解除阻塞的client放入到moduleUnblockedClients鏈表中,后續交由redis內核處理。


五、module命令實現

接着說一下module命令中load、unload、list等實現

首先通過配置文件、命令行或者module load命令加載模塊的時候,如下執行

 
 
 
         
  1. /* 加載一個模塊並初始化. 成功返回 C_OK , 失敗返回C_ERR */
  2. int moduleLoad(const char *path, void **module_argv, int module_argc) {
  3.    int (*onload)(void *, void **, int);
  4.    void *handle;
  5.    RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
  6.    
  7.    //加載動態庫
  8.    handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
  9.    if (handle == NULL) {
  10.        return C_ERR;
  11.    }
  12.    //查找動態庫中入口函數RedisModule_OnLoad的地址
  13.    onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
  14.    if (onload == NULL) {
  15.        return C_ERR;
  16.    }
  17.    
  18.    //執行模塊中的RedisModule_OnLoad入口函數
  19.    if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {
  20.        if (ctx.module) moduleFreeModuleStructure(ctx.module);
  21.        dlclose(handle);
  22.        return C_ERR;
  23.    }
  24.    /* Redis module 加載成功,注冊到modules全局字典中 */
  25.    dictAdd(modules,ctx.module->name,ctx.module);
  26.    ctx.module->handle = handle;
  27.    /*注意這里會把ctx釋放掉,后面需要的時候,會根據modules字典中的查找到的模塊信息,構造一個ctx
  28.     *這意味着在模塊函數中的ctx入參是一個堆棧上的變量,
  29.     *例如通過RedisModule_AutoMemory設置ctx自動內存管理的時候,只是當次有效*/
  30.    moduleFreeContext(&ctx);
  31.    return C_OK;
  32. }

module unload命令卸載一個模塊時候,執行如下簡化代碼

 
 
 
         
  1. /* 卸載一個模塊,成功返回C_OK,失敗返回C_ERR */
  2. int moduleUnload(sds name) {
  3.    struct RedisModule *module = dictFetchValue(modules,name);
  4.    if (module == NULL) {
  5.        return REDISMODULE_ERR;
  6.    }
  7.    //如果模塊導入了本地數據類型,則不允許卸載
  8.    if (listLength(module->types)) {
  9.        return REDISMODULE_ERR;
  10.    }
  11.    /* 模塊可以向Redis服務器注冊新的Redis命令,卸載模塊的時候,需要取消之前注冊的命令 */
  12.    unregister_cmds_of_module(module);
  13.    /* 卸載動態庫 */
  14.    if (dlclose(module->handle) == -1) {
  15.        char *error = dlerror();
  16.        if (error == NULL) error = "Unknown error";
  17.    }
  18.    /* 從全局modules字典中刪除模塊 同時釋放module->name*/
  19.    dictDelete(modules,module->name);
  20.    module->name = NULL;
  21.    //釋放module占用的內存
  22.    moduleFreeModuleStructure(module);
  23.    return REDISMODULE_OK;
  24. }

module list命令執行如下簡化代碼

 
 
 
         
  1. /* modules list簡化代碼 */
  2. void moduleList(sds name) {
  3.    dictIterator *di = dictGetIterator(modules);
  4.    dictEntry *de;
  5.    addReplyMultiBulkLen(c,dictSize(modules));
  6.    //遍歷modules字典,獲取每個模塊的名字和版本
  7.    while ((de = dictNext(di)) != NULL) {
  8.        sds name = dictGetKey(de);
  9.        struct RedisModule *module = dictGetVal(de);
  10.        addReplyMultiBulkLen(c,4);
  11.        addReplyBulkCString(c,"name");
  12.        addReplyBulkCBuffer(c,name,sdslen(name));
  13.        addReplyBulkCString(c,"ver");
  14.        addReplyLongLong(c,module->ver);
  15.    }
  16.    dictReleaseIterator(di);
  17. }

六、模塊導出符號與Redis core函數映射

在Redis提供給模塊的API中,API的名字都是類似RedisModule_<funcname>的形式,實際對應Redis core中的RM_<funcname>函數。目前只有一個例外就是RedisModule_Init這個模塊API在Redis core中的名字也是RedisModule_Init。上面我們講過,RedisModule_Init應該是模塊入口RedisModule_OnLoad中第一個調用的函數。而RedisModule_OnLoad的工作就是完成了RedisModule_<funcname>與RM_<funcname>之間的關聯建立關系。

下面我們首先以上面示例模塊中的RedisModule_CreateCommand這個模塊API為例,說明怎么關聯到RM_CreateCommand上的,然后在說明為什么這樣設計。

1、RedisModule_<funcname>與RM_<funcname>關聯建立過程

1.1、首先在Redis啟動的時候,會執行下面的初始化代碼

 
 
 
         
  1. int moduleRegisterApi(const char *funcname, void *funcptr) {
  2.    return dictAdd(server.moduleapi, (char*)funcname, funcptr);
  3. }
  4. #define REGISTER_API(name) \
  5.    moduleRegisterApi("RedisModule_" #name, (void *)(unsigned long)RM_ ## name)
  6. /* Register all the APIs we export. Keep this function at the end of the
  7. * file so that's easy to seek it to add new entries. */
  8. void moduleRegisterCoreAPI(void) {
  9.    server.moduleapi = dictCreate(&moduleAPIDictType,NULL);
  10.    ...
  11.    //其他的接口同樣需要通過REGISTER_API來注冊
  12.    REGISTER_API(CreateCommand);
  13.    REGISTER_API(SetModuleAttribs);
  14.    ...
  15. }

上面代碼等效於

 
 
 
         
  1. //在server.moduleapi中將字符串"RedisModule_<funcname>"與函數RM_<funcname>的地址建立關聯
  2. dictAdd(server.moduleapi, "RedisModule_CreateCommand", RM_CreateCommand)
  3. dictAdd(server.moduleapi, "RedisModule_SetModuleAttribs", RM_SetModuleAttribs)

1.2、在模塊源碼中包含redismodule.h頭文件的時候,會把下面的代碼包含進來

 
 
 
         
  1. #define REDISMODULE_API_FUNC(x) (*x)
  2. //其他的模塊接口同樣需要通過REDISMODULE_API_FUNC來定義與RM_<funcname>一致的函數指針RedisModule_<funcname>
  3. int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
  4. int REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
  5. #define REDISMODULE_GET_API(name) \
  6.    RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name))
  7. static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
  8.    void *getapifuncptr = ((void**)ctx)[0];
  9.    RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;
  10.    ...
  11.    //其他模塊接口同樣需要通過REDISMODULE_GET_API來初始化RedisModule_<funcname>指針
  12.    REDISMODULE_GET_API(CreateCommand);
  13.    REDISMODULE_GET_API(SetModuleAttribs);
  14.    ...
  15.    RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
  16.    return REDISMODULE_OK;
  17. }

上面代碼進行宏展開后等效如下

 
 
 
         
  1. //定義與RM_<funcname>類型一致的函數指針RedisModule_<funcname>
  2. int (*RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
  3. int (*RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
  4. static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
  5.    void *getapifuncptr = ((void**)ctx)[0];
  6.    RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;
  7.    ...
  8.    //其他模塊接口同樣需要通過REDISMODULE_GET_API來初始化RedisModule_<funcname>指針
  9.    RedisModule_GetApi("RedisModule_CreateCommand",((void **)&RedisModule_CreateCommand);
  10.    RedisModule_GetApi("RedisModule_SetModuleAttribs",((void **)&RedisModule_SetModuleAttribs);
  11.    ...
  12.    RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
  13.    return REDISMODULE_OK;
  14. }

1.3、在上面moduleLoad加載模塊的時候,我們看到會傳遞RedisModuleCtx ctx = REDISMODULE_CTX_INIT作為入參,調用RedisModule_OnLoad,並在RedisModule_OnLoad中調用RedisModule_Init。

 
 
 
         
  1. #define REDISMODULE_CTX_INIT {(void*)(unsigned long)&RM_GetApi, NULL, NULL, NULL, 0, 0, 0, NULL, 0, NULL, NULL, 0, NULL}
  2. /* 查找模塊請求的API,並保存在targetPtrPtr中 */
  3. int RM_GetApi(const char *funcname, void **targetPtrPtr) {
  4.    dictEntry *he = dictFind(server.moduleapi, funcname);
  5.    if (!he) return REDISMODULE_ERR;
  6.    *targetPtrPtr = dictGetVal(he);
  7.    return REDISMODULE_OK;
  8. }

因此在函數RedisModule_Init實際執行的時候,相當於把RedisModule_<funcname>指針初始化為RM_<funcname>函數的地址了。因此隨后在模塊中調用RedisModule_<funcname>的時候,實際上調用的是RM_<funcname>。

2、為什么采用這種設計?

實際上在redismodule.h頭文件或者模塊源碼中直接extern RM_<funcname>,也是可以直接訪問RM_<funcname>這個函數的。那么為什么要在每個模塊的源碼中定一個指向RM_<funcname>的函數指針RedisModule_<funcname>,並通過RedisModule_<funcname>來訪問模塊API呢?


主要是考慮到后續升級的靈活性,模塊可以有不同的API版本,雖然目前API版本只有一個,但是假如后續升級后,Redis支持了新版本的API。那么當不同API版本的模塊向Redis注冊的時候,Redis內核就可以根據注冊的API版本,來把不同模塊中的函數指針指向不同的API實現函數了。這類似以面向對象中依賴於抽象而不是依賴具體的設計思路。


補充說明:

1、在redis源碼src/modules目錄下給出了一些redis模塊相關的示例和說明文檔,是不錯的學習資料。

2、https://github.com/antirez/redis/commit/85919f80ed675dad7f2bee25018fec2833b8bbde







免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM