ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Redis 知识梳理(3)

Redis 知识梳理(3)

作者:没有四次元口袋的蓝胖
日期: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 头部(原子操作,常用于消息队列)RPOPLPUSHsourcedest

1.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 截取
时间线/TimelineLPUSH 最新在最前
弹幕系统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# 2

2.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 s2

2.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-inf50

Java 代码示例:

// 玩家加分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

跳表的核心思想:

  1. 底层(Level 1)是一个完整有序链表
  2. 往上每一层随机"抽取"一部分节点作为"快速通道"
  3. 查询时从最高层开始,遇到比目标大的就降一层继续往前找

为什么不用平衡树(如红黑树)?

对比跳表平衡树(红黑树)
实现复杂度简单复杂(旋转、平衡维护)
范围查询快(直接遍历链表)较慢
并发友好容易实现无锁难
内存开销每个节点维护多层指针,略高相对较低
查找效率O(log n)O(log n)

为什么 Redis 选跳表?

  1. 实现简单,可读性强
  2. 范围查询效率高(找到起点后直接遍历底层链表)
  3. 更容易调参(通过修改层数概率调整性能)

🎯面试高频:ZSet 为什么用跳表而不是红黑树?

核心三点:①实现简单;②范围查询高效;③内存友好(跳表每层节点随机保留)。

3.6 ZSet 性能分析

操作时间复杂度
ZADDO(log N)
ZSCOREO(1)(hashtable)
ZRANKO(log N)
ZRANGEBYSCOREO(log N + M)(M 是结果数)
ZINCRBYO(log N)
ZCARDO(1)
ZCOUNTO(log N)

四、List/Set/ZSet 对比总结

特性ListSetZSet
有序性有序(按插入)无序有序(按分数)
唯一性可重复不重复不重复
主要操作两端插入/弹出集合运算按分数范围查询
底层结构quicklistintset/hashtableziplist/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 是面试的宠儿——不仅考命令,还考底层跳表和排行榜设计。

三个记忆锚点:

  1. List→ 两端操作 + 队列
  2. Set→ 去重 + 集合运算
  3. ZSet→ 分数排序 + 排行榜 + 跳表

跳表是加分项,能把"跳表 vs 红黑树"讲清楚,面试直接加分。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进