《玩转Redis》系列文章 by zxiaofan主要讲述Redis的基础及中高级应用,穿插企业实战案例。本文是《玩转Redis》系列第【16】篇,最新系列文章请前往 公众号 “zxiaofan”(点我点我)查看,或 百度搜索 “玩转Redis zxiaofan”(点我点我)即可。
本文关键字:玩转Redis、Lua脚本入门到实战、树形结构、树形结构的解决方案、“邻接表”、“路径枚举”、查找部门的所有上级部门;
往期精选:《玩转Redis-干掉钉子户-没有设置过期时间的key》
大纲
- 树形结构的常见场景及解决方案
- 树形结构的常见场景
- 树形结构的解决方案:“邻接表”、“路径枚举”
- Redis下树形结构的存储
- Redis中如何使用Lua脚本
- Redis支持的Lua脚本命令简述
- Redis支持的Lua脚本命令详解
- Redis如何调试Lua脚本
- Lua脚本实战Redis树形结构
- Redis中使用Lua脚本的注意事项
前言:
在toB公司负责中台业务,众多企业的部门关系是树形结构,前段时间有业务诉求是“在大数据量下高效查询指定部门的所有上级部门,企业的部门树形关系可能随时变更”。在MySQL的基础上遂想到了利用Redis缓存树形结构并实现高效查询。
1、树形结构的常见场景及解决方案
1.1、树形结构的常见场景
生活中我们有很多树形结构的数据场景,比如:
- 国家行政区域编码;
- 企业组织架构;
树形结构数据的特征是 有明显的所属关系,比如 “行政区域编码”示例中“朝阳区”属于“北京市”,“组织架构”示例中“社交产品部”属于“产品中心”。
1.2、树形结构的解决方案
1.2.1、邻接表
业界最常使用的方案恐怕就是“邻接表”了,简而言之,“邻接表”的每条数据都存储了“上级数据ID”。
数据ID | 数据名称 | 上级数据ID |
---|---|---|
cpzx | 产品中心 | 总公司ID |
sjcpb | 社交产品部 | cpzx |
bgcpb | 办公产品部 | cpzx |
bgcpyz | 办公产品一组 | bgcpb |
bgcpez | 办公产品二组 | bgcpb |
“邻接表”的优点:
- 添加数据高效;
- 修改数据的上级高效;
- 删除叶子节点数据高效;
“邻接表”的缺点:
- 删除中间节点需移动其子节点;
- 查询节点的所有叶子节点、所有父节点复杂;
- 这里指MySQL,Oracle是支持递归查询的;
1.2.2、路径枚举
另一个比较常用的方案就是“路径枚举”了,其核心思想是,每条数据都有字段存储了其所有的上级信息。
数据ID | 数据名称 | 路径 |
---|---|---|
1 | 中国 | 1 |
11 | 北京市 | 1,11 |
110105 | 朝阳区 | 1,11,110105 |
51 | 四川省 | 1,51 |
5101 | 成都市 | 1,51,5101 |
510104 | 锦江区 | 1,51,5101,510104 |
“路径枚举”的优点:
- 查询节点的所有父节点高效;
- select 路径 where 数据ID = ‘节点ID’;
- 查询节点的所有子节点高效;
- select 数据ID where 路径 like ‘1,51%’;
“路径枚举”的缺点:
- 依赖复杂逻辑维护路径;
- 路径长度可能失控;
- 非叶子节点删除或变更上级节点时,所有子节点都将变动;
除了“邻接表”、“路径枚举”,还有存储子孙节点范围的“嵌套集”、维护独立数据表存储所有子孙节点关系的“闭包表”等方案用于存储树形结构数据。由于不是本文重点,此处不再赘述。
在实际的生产方案中,我们也不用拘泥于以上某个方案,适当的将方案整合使用,往往事半功倍。比如我们的生产系统,就有将“邻接表”、“路径枚举”方案混合使用的场景,综合性能也相当出色。
不管黑猫白猫,抓到耗子就是好猫。
2、Redis下树形结构的存储
在阐述存储方案前,我们先详细梳理下现有的业务场景,技术都是为业务服务的。
toB系统,系统中有众多企业(company1 ~ company666),每个企业都有自己的部门树。示例:某公司 A0 下有 B1 ~ B50 这 50 个一级部门;每个一级部门 下 又有若干 个二级部门(比如 B1 下 有 CB1-1 ~ CB1-30 这 30 个二级部门,B3 下 有 CB3-1 ~ CB3-40 这 40 个二级部门);同理,每个二级部门下又有若干个三级部门。对于大企业而言,部门达到几千上万。此外需要注意的是,企业的部门信息是可能随时变动的。而现在我们的诉求是:查询 第N级某个部门的所有上级部门信息。
在MySQL场景下,我们可以“邻接表”或者“路径枚举”方案,甚至于像上面提及的需要做方案混合。对于诉求“查询节点的所有父节点”,通过先前的方案分析,“路径枚举”是较优的方案。但如果当QPS很高,需要进一步提升性能呢,除了提升DB的性能响应外,我们是否还有其他的出路?
高性能的Redis进入了我们的视野,那么如何使用Redis完成树型结构的存储呢?
此处我们使用的Redis数据结构是 Hash,Redis的key为企业ID(depttree:企业ID),field 为 部门ID,field 对应的value是 该部门ID对应的上级部门ID。示例如下:
业务逻辑:
- 查询所有父部门时,先从缓存中查询,缓存缺失时从DB查询并更新到Redis;
- 部门关系变更时,则删除Redis缓存;
- 部门删除时,则删除Redis缓存;
- Redis中的数据存储采用的是“邻接表”的方式;
- 由于任意部门的父部门都可能变动,Redis中的数据存储不采用“路径枚举”方案;
需要注意的是:
- 更新Redis时采用批量更新提升性能,HMSET key field value [field value …];
- 实际生产中,我们采用的是二级缓存,方案更复杂,此处不展开;
HMSET depttree:企业001 B1 A0 B2 A0 B3 A0 CB1-1 B1
3、Redis中如何使用Lua脚本
在上一节中部门关系数据已经存到Redis了,从hash的结构看,无法一次性查询指定部门的所有上级部门,所以我们需要使用到 Lua 脚本。正式实战之前,我们先学习下Redis中如何使用 Lua 脚本。
Redis 2.6.0 版本开始支持 Lua 脚本。Redis中使用 Lua 脚本应直接提供程序体,不需要也不能定义一个函数。下面我们来开始Redis Lua脚本的入门吧。
3.1、Redis支持的Lua脚本命令简述
命令 | 功能 | 参数 |
---|---|---|
EVAL | 执行Lua脚本 | EVAL script numkeys key [key …] arg [arg …] |
SCRIPT LOAD | 将脚本内容导入Redis的脚本缓存 | SCRIPT LOAD script |
EVALSHA | 通过导入脚本的SHA1摘要值执行脚本 | EVALSHA sha1 numkeys key [key …] arg [arg …] |
SCRIPT EXISTS | 判断指定SHA1摘要值的脚本是否存在 | SCRIPT EXISTS sha1 [sha1 …] |
SCRIPT FLUSH | 清空所有的Lua脚本缓存 | SCRIPT FLUSH |
SCRIPT KILL | 杀死正在执行的没有写操作的Lua脚本 | SCRIPT KILL |
SCRIPT DEBUG | 设置Lua脚本debug模式 | SCRIPT DEBUG YES | SYNC | NO |
3.2、Redis支持的Lua脚本命令详解
3.2.1、EVAL
- 参数
- EVAL script numkeys key [key …] arg [arg …]
- 功能
- 执行Lua脚本
- 可用版本
- 2.6.0
- 时间复杂度
- 取决于执行的脚本;
- 参数说明
- script:脚本内容;
- numkeys:key的个数;
- [key …]:key值,个数必须和numkeys匹配;
- [arg …]:附加参数,[key …]后的均为附加参数,个数不固定;
- 返回值
- 脚本执行结果;
- 备注
- Lua 脚本中通过 KEYS[1]、KEYS[2]、ARGV[1] 获取传入的参数;
127.0.0.1:6379> eval "return redis.call('set','公众号','zxiaofan')" 0
OK
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK
3.2.2、SCRIPT LOAD
- 参数
- SCRIPT LOAD script
- 功能
- 将脚本内容导入Redis的脚本缓存;
- 可用版本
- 2.6.0
- 时间复杂度
- O(N),N取决于脚本字节长度;
- 参数说明
- script:脚本内容;
- 返回值
- 导入脚本的SHA1摘要值;