2. 核心指令深度解析:基本命令但没那么简单
2.1 Set基本指令:SADD到SPOP全梳理
先来最常用的几个。我习惯把Set指令分成“基础增删查”和“集合运算”两组来记,这样在脑子里会形成一张清晰的指令地图。
基础组:
- SADD key member [member ...]:向集合添加一个或多个成员。已存在的成员会被忽略,返回实际新添加的数量。
- SREM key member [member ...]:移除一个或多个成员,返回实际移除的数量。
- SMEMBERS key:返回集合中所有成员。这个命令要小心,后面排错章节我会重点说,它是一个O(N)命令,大Key下就是事故隐患。
- SISMEMBER key member:判断成员是否存在,返回1/0。O(1)复杂度,性能极好,日常用得比SMEMBERS还多。
- SCARD key:返回集合的基数(元素数量),O(1)。
- SRANDMEMBER key [count]:随机返回一个或多个元素,不删除。count为正数时返回不重复元素,为负数时可能重复。
- SPOP key [count]:随机移除并返回一个或多个元素。这个“移除”语义很关键,和SRANDMEMBER的区别是面试常问点。
运算组:
- SINTER key [key ...]:返回多个集合的交集。
- SUNION key [key ...]:返回并集。
- SDIFF key [key ...]:返回差集,以第一个集合为基准。
- SINTERSTORE / SUNIONSTORE / SDIFFSTORE:把运算结果存到目标Key,避免在大结果集上反复传输。
这些指令看着简单,但组合起来能覆盖大量真实业务场景。比如电商平台的“商品筛选”,用SINTER把“分类为数码”和“价格区间1000-3000”和“品牌为某品牌”三个集合求交集,比在数据库里做多条件JOIN要快不少,尤其当每个条件都命中几万条数据时,内存集合运算的优势非常明显。
2.2 ZSet基本指令:ZADD到ZRANGEBYSCORE
ZSet指令的记忆点在于:所有操作都围绕一个“分数”展开。
- ZADD key score member [score member ...]:添加成员并指定分数。如果成员已存在,则更新分数。
- ZREM key member [member ...]:移除成员。
- ZSCORE key member:查询某个成员的分数。
- ZINCRBY key increment member:给某个成员的分数增加increment,这个命令是原子操作,做积分累加场景非常顺手,并发下不会丢更新。
- ZCARD key:返回有序集合的元素数量。
- ZRANK key member:返回成员按分数从低到高排序的排名(从0开始)。
- ZREVRANK key member:返回成员按分数从高到低排序的排名。做排行榜时用的是这个。
- ZRANGE key start stop [WITHSCORES]:返回指定索引区间内的成员,按分数低到高。索引支持负数,-1表示最后一个。
- ZREVRANGE key start stop [WITHSCORES]:从高到低返回。排行榜接口的核心命令。
- ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]:按分数范围返回成员。用来做时间范围查询、工资区间过滤等。
- ZCOUNT key min max:统计分数区间内的元素个数。
- ZREMRANGEBYSCORE key min max:按分数范围批量删除。
- ZREMRANGEBYRANK key start stop:按排名区间批量删除,常用于保留Top N,清理尾部数据。
用ZSet做排行榜,ZADD写入用户分数,ZREVRANGE 0 9取前十名,ZSCORE查单用户分数,ZRANK查用户名次,一套组合拳就能把排行榜功能支撑起来,不需要复杂SQL和定时任务,全部O(logN)级别搞定。这个体验是其他数据结构很难替代的。
2.3 背后的机制解读:为什么ZSet操作这么快
Set的原理相对直观:底层是哈希表(或整数集合,满足条件时自动优化),哈希表的增删查都是O(1)。所以SISMEMBER、SADD本身就该快,这是哈希表的基本功。
ZSet就更讲究了。它内部是一个“跳跃表 + 哈希表”的组合结构。哈希表负责按成员快速定位分数,所以ZSCORE可以O(1)返回;跳跃表负责维护成员按分数排序的序列,所以ZRANK、ZRANGE这类涉及排名的操作能在O(logN)时间内完成。
跳跃表可以理解成“多级索引的链表”:底层链表存所有元素,上层每间隔若干元素抽一层作为索引,查找时从高层索引开始快速跨越,跳过大量不需要遍历的节点。它比平衡树写起来简单,实现上又足够高效,Redis作者选用它是有道理的。这也是为什么ZSet能做到“既有唯一成员约束,又有排序能力”,两者兼顾在内存数据结构里其实不太容易。
看编码类型可以用OBJECT ENCODING key。Set在元素少且为整数时是intset,超过阈值变成hashtable;ZSet在元素少于128个且元素长度小于64字节时是ziplist,超过后升级为skiplist。这里有个性能优化的观察点:小数据量时压缩结构省内存,大数据量时跳表保性能,Redis在内部自动做了取舍。了解这个机制,排查某些Key量级突变导致内存上涨时,思路会清晰很多。