Redis 高级数据类型精讲:set/zset 核心命令 + 场景 + 其他类型与渐进式遍历----《Hello Redis!》(3)

发布时间:2026/8/18 18:25:12
Redis 高级数据类型精讲:set/zset 核心命令 + 场景 + 其他类型与渐进式遍历----《Hello Redis!》(3) 文章目录前言set(集合)有关set的命令set在Redis中的应用场景zset类型(有序集合)有关zset的命令zset在Redis中的应用场景其他类型渐进式遍历前言在上一篇文章中我们深入学习了 Redisstring、hash、list三大基础数据类型掌握了缓存、对象存储、简单队列等核心场景的实现方式。而在实际开发中去重统计、社交关系、实时排行榜、精准推荐等高频业务需要更专业的数据结构支撑 ——set无序集合与zset有序集合正是解决这类问题的利器。set 以无序、去重为核心特性搭配交集、并集、差集等集合运算可轻松实现用户标签、共同好友、UV 统计等功能zset 在 set 的基础上增加score 分数维度实现元素自动排序是排行榜、优先级队列、限时任务等场景的最优解。除此之外Redis 还提供了 stream、geo、HyperLogLog、Bitmap 等专用类型针对消息队列、地理位置、海量统计、位存储做了极致优化。同时生产环境中禁止使用keys、smembers等全量遍历命令* 渐进式遍历scan** 成为安全遍历数据的唯一方案。本文将完整拆解set 与 zset 的特性、底层编码、高频命令、业务场景简要介绍 Redis 其他专用类型并详解 scan 渐进式遍历的使用规则与注意事项帮你掌握 Redis 高级数据能力写出安全、高效的生产级代码。set(集合)集合就是一个key里面可以有多个memberset的特性:1.集合中的元素式无序的 2.集合中的元素是不能重复的 3.集合里面的元素都是string类型的(跟List一样)关于set的内部编码:当元素均为整数并且元素个数不是很多的时候用intset;不然的话就用hashtable有关set的命令sadd:将一个或多个元素添加到集合中自动去重重复元素无法添加 语法:sadd key member1 [member2 ...]返回值:本次添加成功的元素个数 时间复杂度:O(1)smembers:获取集合中的所有元素元素顺序无序 语法:smembers key返回值:集合中的所有元素列表 时间复杂度:O(N)sismember:判断元素是否存在于集合中 语法:sismember key member返回值:1表示元素存在0表示元素不存在或key不存在 时间复杂度:O(1)scard:获取集合的元素个数 语法:scard key返回值:集合内的元素个数 时间复杂度:O(1)spop:从集合中删除并返回一个或多个随机元素元素取出后会从集合中移除 语法:spop key [count]返回值:取出的元素列表 时间复杂度:O(N), N为count指定的数量–不指定count时默认是取出一个元素–注意:这个取出的元素是随机的smove:将元素从源集合移动到目标集合中 语法:smove 源集合 目标集合 member返回值:1表示移动成功0表示移动失败 时间复杂度:O(1)srem:从集合中删除指定的一个或多个元素 语法:srem key member1 [member2 ...]返回值:本次操作成功删除的元素个数 时间复杂度:O(N), N为要删除的元素个数有关集合间的操作:(交并差补)sinter:获取多个集合的交集元素返回同时存在于所有集合中的元素 语法:sinter key1 [key2 ...]返回值:交集的元素列表 时间复杂度:O(N*M), N为最小集合元素个数M为最大集合元素个数sinterstore:获取多个集合的交集元素并将结果保存到目标集合中 语法:sinterstore 目标集合 key1 [key2 ...]返回值:交集的元素个数 时间复杂度:O(N*M), N为最小集合元素个数M为最大集合元素个数sunion:获取多个集合的并集元素返回所有集合中不重复的全部元素 语法:sunion key1 [key2...]返回值:并集的元素列表 时间复杂度:O(N), N为所有集合的总元素个数sunionstore:获取多个集合的并集元素并将结果保存到目标集合中 语法:sunionstore 目标集合 key [key ...]返回值:并集的元素个数 时间复杂度:O(N), N为所有给定集合的总元素个数sdiff:获取多个集合的差集元素返回仅存在于第一个集合且不存在于其他所有集合中的元素 语法:sdiff key1 [key2 ...]返回值:差集的元素列表 时间复杂度:O(N), N为所有给定集合的总元素个数sdiffstore:获取第一个集合且不存在于其他所有集合中的元素并将结果保存到目标集合中语法:sdiffstore destination key1 [key2...]返回值:差集的元素个数 时间复杂度:O(N), N为所有给定集合的总元素个数–这上面的时间复杂度是怎么得出的跟内部的源码设计有关了set在Redis中的应用场景1.在Redis中用set存用户标签(也就是做用户画像)2.在Redis中用set算共同好友3.用set统计去重访问量zset类型(有序集合)有序集合就是一个key里面有多个member然后每个member还有一个score这个有序集合的顺序就是依据这个score去排的score跟member的关系有点像pair而不是键值对zset的特性1.集合中的元素是有序的2.member不允许重复但是score可以重复3.有序集合里的member都是string类型的score是浮点型的zset底层数据结构的默认排序规则:(内部是按升序排序的)分数不同按照分数来排序分数相同按照元素自身字符串的字典序来排序如果修改的分数影响到了之前的顺序就会自动移动元素位置保持原有的升序顺序不变关于zset的内部编码:如果有序集合中的元素个数较少并且单个元素体积较小用ziplist来存;其他情况用skiplist来存储–具体的边界是可以去配置文件里面自行修改的关于跳表:跳表本质上就是一个复杂链表,但是查询元素的时间复杂度是O(logN)有关zset的命令zadd:添加或更新有序集合中的元素及关联分数语法:zadd key [nx | xx] [lt | gt] [ch] [incr] score member [score1 member1 ...][nx|xx]:不选选项时:如果当前member不存在就添加member;如果当前member已经存在此时就会更新分数 nx:当前member不存在才进行操作 xx:当前member存在才进行操作[lt|gt]:lt:对于已存在的元素新分数元素当前分数时才更新分数;对于不存在的元素如果无[nx|xx]添加元素 gt:对于已存在的元素新分数元素当前分数时才更新分数;对于不存在的元素如果无[nx|xx]添加元素 incr:给元素的分数加上指定的分数--跟[lt|gt]一起用时:lt或gt看的是算出来的新分数和原来的分数进行比较返回值:默认返回本次新增的元素个数使用ch选项后返回新增更新的元素总数时间复杂度:O(logN)N为集合元素数量时间复杂度不是O(1)的原因:因为是全局有序的zcard:获取有序集合的元素总数 语法:zcard key返回值:集合中所有元素的总个数 时间复杂度:O(1)zcount:统计有序集合中分数在指定区间内的元素个数 语法:zcount key min max返回值:分数满足[min, max]条件的元素个数 时间复杂度:O(logN)N为集合元素数量–支持使用-inf和inf来作为min和max–也支持使用(改成开区间zcount能做到时间复杂度是O(logN)的原因:zset内部会记录每个元素当前的排行/次序所以查找到元素就指定的元素所在的次序(下标)所以找min和max对应的元素然后算出它的次序最后做减法就行了引申:负无穷大和无穷小不是一个概念 --无穷小是无限趋近于0zrange:按下标区间获取有序集合的元素(按分数升序返回) 语法:zrange key start stop [withscores]返回值:[start,stop]内的元素列表,加上withscores的话分数也会连带返回(支持负数下标)时间复杂度:O(logN M)N为集合元素总数M为返回的元素数量返回值虽然是闭区间但是可以通过加括号给它搞成开区间 eg:(start这样 注意:stop)不行只能写成(stop!!!zrevrange:按下标区间获取有序集合的元素(按分数降序返回) 语法:zrevrange key start stop [withscores]返回值:[start,stop]内的元素列表,加上withscores的话分数也会连带返回(支持负数下标)时间复杂度:O(logN M)N为集合元素总数M为返回的元素数量–注意:Redis6.2.0之后就把zrevrange的降序查询功能合并给zrange里了zrangebyscore:按分数区间获取有序集合的元素(按分数升序返回) 语法:zrangebyscore key min max [withscores]返回值:范围在[min,max]内的元素列表,加上withscores的话分数也会连带返回–如果想开区间的话也能用(时间复杂度:O(logN M)N为集合元素总数M为返回的元素数量zpopmax:删除并返回有序集合中分数最高的count个元素语法:zpopmax key [count]如果不写count的话默认count是1返回值:元素与分数成对列表如 [“three”, “3”]时间复杂度:O(log(N) * M)N为集合元素总数M为删除的元素数量–zpopmin是删除并返回有序集合中分数最低的count个元素 其他的跟zpopmax一样–bzpopmin和bzpopmax是他们的阻塞版本–如果是同分数的情况的话zpopmax和bzpopmax会优先删除字典序大的zpopmin和bzpopmax会优先删除字典序小的bzpopmax:zpopmax的阻塞版本删除并返回有序集合中分数最高的1个元素无元素时阻塞等待语法:bzpopmax key1 [key2 ...] timeout这里的timeout单位是s支持小数形式返回值:「key名, 元素, 分数」三元组无元素时返回nil时间复杂度:O(logN)N为集合元素总数–bzpopmin:zpopmin的阻塞版本删除并返回有序集合中分数最低的1个元素无元素时阻塞等待(其他的跟bzpopmax一样)关于Redis这里为啥不把最后一个或者最前那一个元素记录下来让尾删和头删成O(1)1.虽然redis源码中针对有序集合记录了头部和尾部这样的特殊位置但是在实际删除的时候调用的是通用的删除函数所以没优化成O(1)2.还有就是因为O(logN)其实不算很慢 3.而且因为zset底层是跳表所以不行模拟阻塞队列的话可以用list模拟阻塞的优先级队列的话可以用zsetzrank:返回zset中指定元素在升序排列下的下标(分数越小排名越靠前) 语法:zrank key member返回值:元素的排名下标0开始元素不存在时返回 nil 时间复杂度:O(logN)N为集合元素总数zrevrank:返回zset中指定元素在降序排列下的下标(分数越高排名越靠前) 语法:zrevrank key member返回值:元素的排名下标0开始元素不存在时返回 nil 时间复杂度:O(logN)N为集合元素总数zscore:获取zset中指定元素的分数 语法:zscore key member返回值:元素的分数元素不存在时返回 nil 时间复杂度:O(1)为啥zscore是O(1)zrank是O(logN):zscore是直接去查哈希表;zrank是因为哈希表里只存了member和score不知道排名所以需要去跳表里面找zrem:删除Zset中一个或多个指定元素按member精确匹配 语法:zrem key member1 [member2 ...]返回值:成功删除的元素个数不存在的元素不计入结果时间复杂度:O(M*logN)M为删除的元素数N为集合总元素数zremrangebyrank:按升序下标区间删除有序集合元素下标从0开始区间为闭区间[start, stop]语法:zremrangebyrank key start stop返回值:成功删除的元素个数时间复杂度:O(logN M)N为集合总元素数M为区间包含的元素个数zremrangebyscore:按分数区间([min,max])删除有序集合元素支持(语法:zremrangebyscore key min max返回值:成功删除的元素个数时间复杂度:O(logN M)N为集合总元素数M为区间包含的元素个数zincrby:为指定的元素对应的分数添加指定的分数(increment可以是负数)语法:zincrby key increment member返回值:操作后元素的新分数 时间复杂度:O(logN)N为集合总元素数–如果member不存在的话会自动创建(key也一样)–这个操作不光会修改分数同时还会移动元素位置让整个有序集合仍然是升序的注意:zset里按分数操作的命令才支持(按下标操作的命令不支持(zset集合间操作的命令:zinterstore:求多个有序集合的交集并存入目标集合中语法:zinterstore 目标集合 numkeys key1 [key2...] [weights 权重1 ...] [aggregate sum|min|max]关于其中选项的介绍:numkeys:说明有几个key--需要这个选项的原因:避免Redis把选项和keys混淆[weights 权重1...]:设置各个输入集合的权重不设置的话默认是1[aggregatesum|min|max]:如果不选的话默认是sum--这个就是在有元素member相同但是score不同时对score的取舍--sum的话就是根据权重来求summin和max就是根据权重后的结果来取舍--eg:zset1有个元素score是10zset2有个元素score是20如果他们的menber相同-他俩默认情况下的交集的score就是30返回值:目标集合的元素个数时间复杂度:O(N*K M*logM)N为输入集合中最小的元素个数K为输入集合的个数M为结果集合的元素个数-这个时间复杂度可以近似成O(M*logM)–这些时间复杂度一般是不用记得面试也不会问这些–注意:zinter,zunion和zdiff是Redis6.2.0版本才开始支持的有序集合命令zunionstore:求多个有序集合的并集并存入目标集合中语法:zunionstore 目标集合 numkeys key1 [key2...] [weights 权重1 ...] [aggregate sum|min|max]–(其他的跟zinterstore差不多)ZUNIONSTORE:计算多个有序集合的并集并保存到目标集合支持分数权重与聚合方式默认求和 语法: ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight …] [AGGREGATE SUM|MIN|MAX] 返回值:目标集合的元素个数 时间复杂度:O(N M*logM)N为所有输入集合的总元素数M为结果集合的元素数zset集合间在比较元素相同时只要member相同就行(eg:求交集时)zset在Redis中的应用场景1.制作需要实时更新的排行榜系统时用zset存数据–只要把玩家信息和对应的分数放到有序集合中的话就自然形成了一个排行榜–如果是那种需要加权处理的数据处理的方法:搞多个key存各个数据最后比如用zinterstore进行加权引申:相关的估算1KB 约等于 一千字节1MB 约等于 一百万字节1GB 约等于 十亿字节其他类型相较于上面那几个类型这里的类型会不常用些但是特定场景用时会特别好用这里介绍:streamgeospatialhyperloglogbitmapsbitfieldsstream类型:本质是一个阻塞的消息队列(比list当阻塞队列好)–生产者发信息然后消费者处理geospatial类型:是Redis专门为地理位置数据设计的有序集合–用来存储经纬度然后就可以让用户给定一个坐标和需要的范围限制然后就可以去刚才存储的点里去找hyperloglog类型:估算集合中不重复元素的个数–应用场景:比如统计网站的独立访客数–这个类型不会存集合中元素本身只会存元素的特征然后用概率算法估算数量(Redis中实现这个的误差差不多0.81%)–这样的话每次都只会占用12KB的内存bitmap类型:用比特位去存储元素(相当于是set类型针对整数的特别优化版本比set更加节省空间)–跟hyperloglog的对比:虽然hyperloglog更加节省空间但是hyperloglog是没有存储元素内容的只存了特征bitfield类型:就是一串二进制序列然后可以自行定义从哪个位置开始用多少个比特位是存的是…还能对这个存储的数据直接进行查改–相较于string或者hash的话会更加的节省空间–bitmap是单比特位的bitfield是多比特位的渐进式遍历这个的话就是每执行一次命令就只获取其中的一小部分key–来保证当前这一次操作不会特别卡–这个遍历是能随时终止的不会对服务器产生任何的副作用如果想要得到所有的key就需要多次执行scan命令–但是要注意:用scan遍历也是有风险的尽量不要做遍历所有key这个操作scan:语法:SCAN cursor [match pattern] [count 单次遍历的数量] [type 类型]关于这里的选项:[match pattern]:跟前面介绍的key的那个pattern一样[count 单次遍历的数量]:限制这一次遍历最多能获取到多少个元素,默认是10--注意:这里的count只是给redis的一个建议实际返回的key的个数不一定是完全相同的(在中间遍历时也会这样)[type 类型]:只返回value类型和指定的类型一致的key--如果不写这个选项的话就是表示不做任何类型的过滤遍历Redis中所有类型的key关于cursor:这个是从哪个位置开始遍历的标记(传0的话表示从头开始遍历)返回值:本地得到的key和下一次cursor应该填啥(如果cursor返回值是0说明已经遍历完毕了)–注意:返回回来的cursor不一定是递增的因为返回回来的cursor不具有可读性是哈希表内部的槽位索引时间复杂度:O(1)注意:不要在边遍历时边修改内容!!!–不仅仅是Redis任何场景下都忌讳这么搞的!比如C用线程安全去解决这个问题