Redis 想必大家都不陌生,提升服务吞吐量之缓存必杀器。支持的数据类型也相对比较丰富,基本的 kvstring,list,hash、set、zset 等等,日常业务需求大部分也都能 cover 住。
But 咱家小主还具备一些高级的扩展技能来应对现有数据结构集无法解决问题的场景哦~~
比如高版本支持的 GEO、Bitmap、Stream(5.0) 等类型就是基于zset、string等基本类型扩展出来的,分别用来帮助实现 LBS、二进制位图、消息队列等功能。
扩展数据类型大致主要有三种方式,基于 Lua 脚本、基于 Module 扩展 及 修改源码的方式,修改源码的方式对开发人员要求相对高一些,我们暂时不展开,接下来我们一起来看下吧~~
Redis 基于 Lua 脚本进行操作主要解决以下场景的问题:
接下来我们针对下面的场景来进行示例:
某电商平台在用户购物后要给用户推荐感兴趣的物品,需要根据物品的销量和关联加购物车的次数进行综合推荐,每次选出销量最高的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 4.0 版本加入了 Modules 非常灵活,大大方便了开发者进行扩展。Module 可以动态的载入和卸载,可以实现底层的数据结构也可以调用高层的指令,这一切都只需要包含头文件 redismodule.h ,和 Redis 本身一样简洁优雅。
官方提供了 Helloworld.c 可以参照扩展,基本都是套模板就可以,也提供了很多扩展出来的 Modules(https://redis.io/modules),如 RedisGraph、RedisSearch、RedisJson、RedisSQL 等,十分全面,可以快速扩展一个类型,大家可以参考。
主要要分三步走:
/* 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(略)
//依次实现各个命令insert、range、len等方法(略)
编码完成后编译出 so 直接在 redis.conf 中 load 模块的 so 文件,或者命令行操作都可以~~
#redis.conf 增加 so 配置
loadmodule /xxxxx/mymodule.so
#redis-cli连接后命令行
module load /xxxxx/mymodule.so
module unload mymodule
module list
方法 | 基于 lua脚本 | 基于module | 基于源码改造 |
---|---|---|---|
性能 | 中 | 较高 | 高 |
扩展复杂度 | 低 | 中 | 高 |
稳定性和维护成本 | 成熟稳定,各redis版本兼容性好、几乎无额外维护成本 | module部分需要较好的质量管理、兼容性好、维护成本主要是module管理部分 | 对开发要求最高,相当于深度定制,后续redis版本更新升级或者patch,都需要在代码级重新改造适配,维护成本除扩展部分外,还需要维护redis部分(最坏的情况涉及内部多版本管理) |
扩展限制 | 只能基于现有redis支持的结构实现组合型结构结构 | 可自行扩展,不受限现有结构 | 结构可自行扩展,不受限现有结构 |
需要了解的部分 | lua语法、lua 调试(redis 3.2后自带lua debugger调试器) | module机制:回调注册,rdb格式、aof机制 redis 命令扩展机制 gdb调试 so 了解redis源码结构 | 较为深刻理解redis源码结构细节,再各个部分嵌入自己业务结构代码(rdb格式、aof机制、redis 命令扩展等) 相对于module方式的较为集中的解决扩展方式,这种方式需要改造点十分离散约需要改动7+个文件 gdb 调试 |
适用场景 | 1. 某些需要redis 复合结构(多个数据类型一起用完成一个功能)的场景,可利用lua简化应用设计和调用放大系数(如某服务需要调用4-10次redis,可以用lua合并为一个) 2. 某些场景下操作redis复合结构需要保证原子性 | 1. redis lua 解决不了的特殊结构 2. 对性能、存储效率要求较高 | 对性能、存储效率要求较高 |
程序猿的世界里各种开发语言层出不穷,有时比换季上新衣的速度都快,但是好在不用像换新衣一样完全彻底更换,结合各自的长处让其尽可能的发挥价值才不枉费,最近项目中 C++ 调用 python、golang 调用 lua、包括本篇谈论的 redis 调用 lua、官方 Modules 里 Rust 实现的扩展或许都是结合场景下的编译语言与脚本语言互调的跨语言产物。本人技术深度有限,涉猎面也不够广,只是刚好用到了这些,有空可以深入分析讨论下~~