Redis 擴展的常見套路

引言

Redis 想必大家都不陌生,提升服務吞吐量之緩存必殺器。支持的數據類型也相對比較豐富,基本的 kvstring,list,hash、set、zset 等等,日常業務需求大部分也都能 cover 住。
But 咱家小主還具備一些高級的擴展技能來應對現有數據結構集無法解決問題的場景哦~~

比如高版本支持的 GEO、Bitmap、Stream(5.0) 等類型就是基於 zset、string 等基本類型擴展出來的,分別用來幫助實現 LBS、二進制位圖、消息隊列等功能。

擴展數據類型大致主要有三種方式,基於 Lua 腳本基於 Module 擴展修改源碼的方式,修改源碼的方式對開發人員要求相對高一些,我們暫時不展開,接下來我們一起來看下吧~~

Redis 執行 Lua 腳本

Redis 基於 Lua 腳本進行操作主要解決以下場景的問題:

  1. 限時秒殺、答題紅包雨等系統瞬時請求量特別高,需要進行系統限流要保證操作原子性等。

  2. 頻繁的 Redis 操作類業務,需要降低網絡開銷,可以將多個請求通過 Lua 腳本的形式一次發送,減少網絡時延。

  3. 同時按多個維度進行存儲調度的數據結構,比如同時按累計訪問量和評分兩個維度進行數據篩選,且瞬時訪問量比較高變化迅速。

接下來我們針對下面的場景來進行示例:

某電商平臺在用戶購物後要給用戶推薦感興趣的物品,需要根據物品的銷量和關聯加購物車的次數進行綜合推薦,每次選出銷量最高的 20 款商品,然後結合關聯加購物車的次數從這 20 款商品中選出 Top 10 進行推薦。

我們藉助 zset 有序集合作爲我們的主結構存儲商品的銷量,用一個 hash 結構存儲商品被加購物車的次數,通過一箇中間 zset 來存儲 綜合銷量和關聯加車次數,最終選出想要的內容。

local action = KEYS[1]
local goods = KEYS[2]
--銷量zset
local qkey_sale = "sale:zset"
--關聯加購物車次數hash
local qkey_add = "add:hash"
--中間zset
local qkey_final = "final:zset"

--購買
if action == "buy" then
   redis.call('zincrby',qkey_sale,1,goods)
--加購物車
elseif action == "add" then
   redis.call('hincrby',qkey_add,goods,1)
end

--匯聚中間集合
local datalist = redis.call('ZRANGEBYSCORE',qkey_sale,0,10000000,"limit",0,20)
for k, v in pairs(datalist) do
    local data = redis.call("hget",qkey_add,v)
    redis.call('zadd',qkey_final,data,v)
end
--推薦結果
local result = redis.call('ZRANGEBYSCORE',qkey_final,0,1000000,"limit",0,10)
redis.call('del',qkey_final)

return result

通過 redis 命令行直接執行這個 lua 腳本即可。

 redis-cli -p 6379 --eval redis_lua.lua add 123

也許,通過程序與 Redis 相結合的方式也能間接搞定,但是我們說了,瞬時變化比較大哈,爲了保證結果的準確性需要確保 redis 相關操作的原子性。
另外,只是爲了演示一下 基於 Lua 腳本來實現 Redis 更強大的擴展功能哈,也許會在別的必須的應用場景下幫助到你吶,也算是好事一樁了~~

這裏也有一些相應的調試工具可以使用,還是很方便的。業界大家用這個來實現令牌桶、IP 限流、答題紅包、限時秒殺等用的比較多,感興趣可以自己嘗試下~~

Redis Modules

Redis 4.0 版本加入了 Modules 非常靈活,大大方便了開發者進行擴展。Module 可以動態的載入和卸載,可以實現底層的數據結構也可以調用高層的指令,這一切都只需要包含頭文件 redismodule.h ,和 Redis 本身一樣簡潔優雅。
官方提供了 Helloworld.c 可以參照擴展,基本都是套模板就可以,也提供了很多擴展出來的 Modules(https://redis.io/modules),如 RedisGraph、RedisSearch、RedisJson、RedisSQL 等,十分全面,可以快速擴展一個類型,大家可以參考。

主要要分三步走:

  1. 創建新數據類型:主要是內存模型的底層實現

  2. 命令實現:實現需要的各種 redis 命令等

  3. Module 適配:註冊和實現框架的基本接口( rdb、aof、load 註冊、free 等函數)

/* This function must be present on each Redis module. It is used in order to
 * register the commands into the Redis server. */
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    REDISMODULE_NOT_USED(argv);
    REDISMODULE_NOT_USED(argc);

    printf("mktype:RedisModule_OnLoad:enter\n");
    if (RedisModule_Init(ctx,"mytesttype",1,REDISMODULE_APIVER_1)
        == REDISMODULE_ERR) return REDISMODULE_ERR;

    RedisModuleTypeMethods tm = {
        .version = REDISMODULE_TYPE_METHOD_VERSION,
        .rdb_load = MKTypeRdbLoad,  //rdb load函數,待實現
        .rdb_save = MKTypeRdbSave,  //rdb save函數,待實現
        .aof_rewrite = MKTypeAofRewrite, //aof rewrite函數,待實現
        .mem_usage = MKTypeMemUsage,  //memusage函數,待實現
        .free = MKTypeFree,  //free函數,待實現

        //可選
        .digest = MKTypeDigest
    };

    printf("mktype:RedisModule_OnLoad:RedisModule_CreateDataType\n");
    MKType = RedisModule_CreateDataType(ctx,"mytesttype",0,&tm);
    if (MKType == NULL) return REDISMODULE_ERR;

    printf("mktype:RedisModule_OnLoad:mktype.insert\n");
    if (RedisModule_CreateCommand(ctx,"mktype.insert",
        MKTypeInsert_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    printf("mktype:RedisModule_OnLoad:mktype.range\n");
    if (RedisModule_CreateCommand(ctx,"mktype.range",
        MKTypeRange_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    printf("mktype:RedisModule_OnLoad:mktype.len\n");
    if (RedisModule_CreateCommand(ctx,"mktype.len",
        MKTypeLen_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    printf("mktype:RedisModule_OnLoad:REDISMODULE_OK\n");
    return REDISMODULE_OK;
}

//依次實現 rdb load/save、aof、free等函數,如果不考慮主從一致性等也可以 Null(略)
//依次實現各個命令insertrangelen等方法(略)

編碼完成後編譯出 so 直接在 redis.conf 中 load 模塊的 so 文件,或者命令行操作都可以~~

#redis.conf 增加 so 配置
loadmodule /xxxxx/mymodule.so

#redis-cli連接後命令行
module load /xxxxx/mymodule.so
module unload mymodule
module list

對比及使用場景分析

GquzXd

思考及發散

程序猿的世界裏各種開發語言層出不窮,有時比換季上新衣的速度都快,但是好在不用像換新衣一樣完全徹底更換,結合各自的長處讓其儘可能的發揮價值纔不枉費,最近項目中 C++ 調用 python、golang 調用 lua、包括本篇談論的 redis 調用 lua、官方 Modules 裏 Rust 實現的擴展或許都是結合場景下的編譯語言與腳本語言互調的跨語言產物。本人技術深度有限,涉獵面也不夠廣,只是剛好用到了這些,有空可以深入分析討論下~~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QQj09RMdLEWhzzrXFHJ1tg