作者:没有四次元口袋的蓝胖
日期:2026-10-13
标签:Redis, List, Set, ZSet, 排行榜
Redis知识梳理(3)
写在前面
上一篇我们搞定了 String 和 Hash 的命令。本篇继续:把剩下的三种类型——List、Set、ZSet过一遍。
这三种类型各有特色:
- List:有序、可重复、支持两端操作,天然适合队列
- Set:无序、不重复,天然去重,能做集合运算
- ZSet:有序集合,每个元素带分数,排行榜的绝对王者
面试中,ZSet 的排行榜场景和底层跳表结构是高频考点,必须讲透。
一、List 类型详解
1.1 List 是什么
List(列表)是一个有序的字符串列表,可以包含重复元素,支持从两端插入和弹出。
类比:
- Java 的
LinkedList - 排队购物:队头、队尾都能加人
- 微信消息列表:最新的在顶部
1.2 基本命令
# 从左边(头部)插入LPUSH list:a b c# 按顺序插入 c, b, a(最左是a)# 列表变成 [a, b, c]# 从右边(尾部)插入RPUSH list:a b c# 按顺序插入 a, b, c(最右是c)# 列表变成 [a, b, c]# 从左边(头部)弹出LPOP list# 返回 "a",并从列表移除# 列表变成 [b, c]# 从右边(尾部)弹出RPOP list# 返回 "c"# 列表变成 [b]# 按范围查看(LRANGE)LRANGE list0-1# 查看所有元素,-1 表示最后一个# 按索引获取单个元素LINDEX list0# 获取第0个元素# 按索引范围截取LRANGE list02# 取第0到第2个# 查看长度LLEN list# 按值删除元素(count>0从左边删,<0从右边删,0删所有)LREM list2"a"# 从左边删除 2 个 "a"# 设置指定索引的值LSET list0"new_value"# 截取列表,只保留指定范围LTRIM list09# 只保留第0到第9个元素1.3 两种队列用法
FIFO 队列(先进先出):
RPUSH queue task1 task2# 任务从右边进LPOP queue# 任务从左边出LIFO 栈(后进先出):
LPUSH stack a b c# 从左边压栈LPOP stack# 从左边弹栈RPOPLPUSH:从一个列表尾部弹,压到另一个列表头部(原子操作)
# 把 source 尾部元素移到 dest 头部(原子操作,常用于消息队列)RPOPLPUSHsourcedest1.4 阻塞操作:BLPOP / BRPOP
# 如果列表为空,会一直阻塞等待(最多等30秒)BLPOP list30BRPOP list30# 阻塞并移动BRPOPLPUSHsourcedest30这是实现"消息队列"的关键命令——消费者可以"挂起"等待新消息,有新消息就立刻处理,不会空轮询。
1.5 List 底层:quicklist
List → quicklist = 多个 ziplist + 双向链表 ┌─────────┐ ┌─────────┐ ┌─────────┐ │ziplist │──▶│ziplist │──▶│ziplist │ │ [a,b,c] │◀──│ [d,e,f] │◀──│ [g,h,i] │ └─────────┘ └─────────┘ └─────────┘每个 ziplist 装几个元素(默认8192字节或64个),双向链表串联它们。这样:
- 两端操作高效
- 内存相对紧凑
- 中间访问也能索引(虽然不如数组快)
1.6 List 典型场景
| 场景 | 实现 |
|---|---|
| 消息队列 | LPUSH + BRPOP / RPOPLPUSH |
| 文章/消息最新列表 | LPUSH + LTRIM 截取 |
| 时间线/Timeline | LPUSH 最新在最前 |
| 弹幕系统 | LPUSH + LRANGE 获取最近N条 |
💡 Redis 5.0 之后引入了 Stream 类型做更专业的消息队列,但 List 依然是经典实现。
二、Set 类型详解
2.1 Set 是什么
Set(集合)是一个无序、不重复的字符串集合。天然去重。
类比:
- Java 的
HashSet - 一群互不相同的苹果
- “共同好友”、“抽奖候选人”
2.2 基本命令
# 添加一个或多个成员SADD set1 a b c# 移除SREM set1 b# 判断成员是否存在(O(1))SISMEMBER set1 a# 1存在 0不存在# 返回所有成员(无序)SMEMBERS set1# ["a", "c"]# 随机返回一个成员(不删除)SRANDMEMBER set1# 随机返回并删除一个成员(抽奖神器)SPOP set1# 查看集合大小SCARD set1# 22.3 集合运算(Set 的杀手锏)
Set 支持两个集合之间的交集、并集、差集运算:
# 两个集合SADD s1 a b c d SADD s2 c d e f# 交集(共同的元素)SINTER s1 s2# ["c", "d"]# 并集(所有元素的合集,去重)SUNION s1 s2# ["a", "b", "c", "d", "e", "f"]# 差集(s1有但s2没有的)SDIFF s1 s2# ["a", "b"]SDIFF s2 s1# ["e", "f"]# 交集存入新集合SINTERSTORE result s1 s2# 并集存入新集合SUNIONSTORE result s1 s2# 差集存入新集合SDIFFSTORE result s1 s22.4 Set 典型场景
| 场景 | 实现 |
|---|---|
| 去重 | SADD(重复的加不进去) |
| 抽奖 | SPOP(随机弹出一个) |
| 共同好友 | SINTER(两用户好友集合取交集) |
| 点赞/收藏集合 | SADD 用户ID,SISMEMBER 判断是否点过 |
| 标签系统 | SADD 标签,SINTER 找有共同标签的 |
| 随机推荐 | SRANDMEMBER |
| 黑白名单 | SADD 白名单,SISMEMBER 判断是否允许 |
共同好友案例:
# 用户1001的好友SADD user:1001:friends 张三 李四 王五# 用户1002的好友SADD user:1002:friends 李四 王五 赵六# 计算共同好友SINTER user:1001:friends user:1002:friends# ["李四", "王五"]2.5 Set 底层结构
全整数且元素少(默认512以内) →intset(整数集合),内存紧凑
否则→hashtable(value 为 null,靠 key 去重)
2.6 常见坑点
坑:SMEMBERS 遍历巨大集合
当集合有百万级成员时,SMEMBERS 会一次性返回所有,阻塞 Redis。
用SSCAN游标分批扫描更安全。
三、ZSet 类型详解(重点!)
3.1 ZSet 是什么
ZSet(Sorted Set,有序集合)是 Set 的升级版——每个元素关联一个分数(score),Redis 按分数从小到大排序。
类比:
- Java 的
TreeSet+ 分数 - 一份成绩单:张三95分、李四88分、王五90分 → 按分数排序
- 一份"排行榜"
ZSet 的核心优势:能高效地按范围/分数查询,同时天然去重。
3.2 基本命令
# 添加元素(带分数)ZADD zset1100a200b50c# 添加三个元素:a(100分) b(200分) c(50分)# 按分数升序查看ZRANGE zset10-1# [c(50), a(100), b(200)]# 按分数降序查看ZREVRANGE zset10-1# [b(200), a(100), c(50)]# 带分数返回ZRANGE zset10-1WITHSCORES# 返回 [c 50, a 100, b 200]# 按分数范围查询(升序)ZRANGEBYSCORE zset10150# [c(50), a(100)]# 按分数范围查询(降序)ZREVRANGEBYSCORE zset11500# [a(100), c(50)]# 按排名查询(从0开始,升序)ZRANGE zset102# 排名0到2# 按排名查询(降序)ZREVRANGE zset102# 最高分前3名# 修改分数(累加)ZINCRBY zset150c# c 的分数 50→100# 获取元素的分数ZSCORE zset1 a# 100# 获取元素的排名(从0开始,升序)ZRANK zset1 a# 1# 获取排名(降序)ZREVRANK zset1 a# 1# 统计分数范围内的元素数量ZCOUNT zset10150# 2# 查看元素数量ZCARD zset1# 3# 删除元素ZREM zset1 c# 按排名范围删除ZREMRANGEBYRANK zset100# 删除排名最低的# 按分数范围删除ZREMRANGEBYSCORE zset10100# 删除分数0~100的3.3 ZSet 排行榜实战(面试核心)
场景:实现一个游戏实时排行榜
# 玩家注册/登录,加入排行榜ZADD game:rank0"player:1001"ZADD game:rank0"player:1002"ZADD game:rank0"player:1003"# 玩家获得分数(累加)ZINCRBY game:rank150"player:1001"# 1001 加 150 分ZINCRBY game:rank200"player:1002"ZINCRBY game:rank100"player:1003"# 查看全球前10名(降序)ZREVRANGE game:rank09WITHSCORES# 查看某个玩家的分数ZSCORE game:rank"player:1001"# 查看某个玩家的排名(降序,第0名是第一名)ZREVRANK game:rank"player:1001"# 查看分数区间内的玩家ZRANGEBYSCORE game:rank100300WITHSCORES# 清理低分玩家(比如删除分数<50的)ZREMRANGEBYSCORE game:rank-inf50Java 代码示例:
// 玩家加分publicvoidaddScore(longuserId,longscore){Stringkey="game:rank";redisTemplate.opsForZSet().incrementScore(key,"player:"+userId,score);}// 获取前N名publicSet<ZSetOperations.TypedTuple<String>>getTopN(longn){returnredisTemplate.opsForZSet().reverseRangeWithScores("game:rank",0,n-1);}// 获取某玩家排名publicLonggetRank(longuserId){Longrank=redisTemplate.opsForZSet().reverseRank("game:rank","player:"+userId);returnrank==null?null:rank+1;// +1 让排名从1开始}3.4 同分排序问题
坑:两个玩家同分,怎么按时间排序?
方案:把分数拼成一个复合分数:
实际分数 = 原始分数 * 10^13 + (时间戳倒数)比如:
- 玩家1: 100分,在时间戳 1720000000000 → 100*10^13 + (9999999999999 - 1720000000000) = 1000000000000000 + 8279999999999
- 玩家2: 100分,在时间戳 1720000001000 → 100*10^13 + (9999999999999 - 1720000100000)
这样玩家1分数更大,排前面。
3.5 ZSet 底层结构(面试必问)
小数据(默认<128 元素,且值<64 字节):
- 用ziplist(压缩列表)
- 元素按分数从小到大排列
大数据:
- 用skiplist(跳表)+ hashtable
- 跳表负责有序遍历和范围查询
- hashtable 负责 O(1) 查分数
3.5.1 跳表(Skip List)详解
跳表是 ZSet 的核心数据结构,面试必问。
跳表结构示意: Level 3: 1 ──────────────────────────────────────▶ 9 Level 2: 1 ────────▶ 3 ────────▶ 5 ────────▶ 9 Level 1: 1 ─▶ 2 ─▶ 3 ─▶ 4 ─▶ 5 ─▶ 6 ─▶ 7 ─▶ 8 ─▶ 9跳表的核心思想:
- 底层(Level 1)是一个完整有序链表
- 往上每一层随机"抽取"一部分节点作为"快速通道"
- 查询时从最高层开始,遇到比目标大的就降一层继续往前找
为什么不用平衡树(如红黑树)?
| 对比 | 跳表 | 平衡树(红黑树) |
|---|---|---|
| 实现复杂度 | 简单 | 复杂(旋转、平衡维护) |
| 范围查询 | 快(直接遍历链表) | 较慢 |
| 并发友好 | 容易实现无锁 | 难 |
| 内存开销 | 每个节点维护多层指针,略高 | 相对较低 |
| 查找效率 | O(log n) | O(log n) |
为什么 Redis 选跳表?
- 实现简单,可读性强
- 范围查询效率高(找到起点后直接遍历底层链表)
- 更容易调参(通过修改层数概率调整性能)
🎯面试高频:ZSet 为什么用跳表而不是红黑树?
核心三点:①实现简单;②范围查询高效;③内存友好(跳表每层节点随机保留)。
3.6 ZSet 性能分析
| 操作 | 时间复杂度 |
|---|---|
| ZADD | O(log N) |
| ZSCORE | O(1)(hashtable) |
| ZRANK | O(log N) |
| ZRANGEBYSCORE | O(log N + M)(M 是结果数) |
| ZINCRBY | O(log N) |
| ZCARD | O(1) |
| ZCOUNT | O(log N) |
四、List/Set/ZSet 对比总结
| 特性 | List | Set | ZSet |
|---|---|---|---|
| 有序性 | 有序(按插入) | 无序 | 有序(按分数) |
| 唯一性 | 可重复 | 不重复 | 不重复 |
| 主要操作 | 两端插入/弹出 | 集合运算 | 按分数范围查询 |
| 底层结构 | quicklist | intset/hashtable | ziplist/skiplist+hashtable |
| 典型场景 | 队列/最新消息 | 去重/共同好友 | 排行榜/延时队列 |
| 计数能力 | 无 | 无 | 分数可累加 |
| 复杂度 | 两端 O(1) | 添加/查询 O(1) | O(log N) |
思维导图速览
List/Set/ZSet 命令 + 排行榜实战 │ ├── List(有序、可重复、两端操作) │ ├── LPUSH/RPUSH/LPOP/RPOP/LRANGE │ ├── LLEN/LINDEX/LREM/LSET/LTRIM │ ├── BLPOP/BRPOP(阻塞) │ ├── RPOPLPUSH(原子移动) │ ├── 底层:quicklist │ └── 场景:队列/最新消息/Timeline │ ├── Set(无序、不重复、集合运算) │ ├── SADD/SREM/SISMEMBER/SMEMBERS │ ├── SPOP/SRANDMEMBER │ ├── SINTER/SUNION/SDIFF(交集并集差集) │ ├── SSCAN(游标扫描) │ ├── 底层:intset/hashtable │ └── 场景:去重/抽奖/共同好友/点赞 │ ├── ZSet(有序集合,按分数排序)⭐ │ ├── ZADD/ZRANGE/ZREVRANGE │ ├── ZRANGEBYSCORE/ZINCRBY/ZSCORE │ ├── ZRANK/ZREVRANK/ZCOUNT/ZCARD │ ├── ZREM/ZREMRANGEBYSCORE │ └── 底层:ziplist / skiplist+hashtable │ ├── 跳表实现 O(log N) 查询 │ └── 范围查询高效 │ └── ZSet 排行榜实战 ├── ZADD 初始化 ├── ZINCRBY 加分 ├── ZREVRANGE 取前N名 ├── ZREVRANK 看玩家排名 └── 同分排序:复合分数方案写在最后
三种类型里,ZSet 是面试的宠儿——不仅考命令,还考底层跳表和排行榜设计。
三个记忆锚点:
- List→ 两端操作 + 队列
- Set→ 去重 + 集合运算
- ZSet→ 分数排序 + 排行榜 + 跳表
跳表是加分项,能把"跳表 vs 红黑树"讲清楚,面试直接加分。