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 基本命令

bash 复制代码
# 从左边(头部)插入
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 list 0 -1           # 查看所有元素,-1 表示最后一个

# 按索引获取单个元素
LINDEX list 0              # 获取第0个元素

# 按索引范围截取
LRANGE list 0 2            # 取第0到第2个

# 查看长度
LLEN list

# 按值删除元素(count>0从左边删,<0从右边删,0删所有)
LREM list 2 "a"            # 从左边删除 2 个 "a"

# 设置指定索引的值
LSET list 0 "new_value"

# 截取列表,只保留指定范围
LTRIM list 0 9             # 只保留第0到第9个元素

1.3 两种队列用法

FIFO 队列(先进先出):

bash 复制代码
RPUSH queue task1 task2    # 任务从右边进
LPOP queue                  # 任务从左边出

LIFO 栈(后进先出):

bash 复制代码
LPUSH stack a b c          # 从左边压栈
LPOP stack                  # 从左边弹栈

RPOPLPUSH:从一个列表尾部弹,压到另一个列表头部(原子操作)

bash 复制代码
# 把 source 尾部元素移到 dest 头部(原子操作,常用于消息队列)
RPOPLPUSH source dest

1.4 阻塞操作:BLPOP / BRPOP

bash 复制代码
# 如果列表为空,会一直阻塞等待(最多等30秒)
BLPOP list 30
BRPOP list 30

# 阻塞并移动
BRPOPLPUSH source dest 30

这是实现"消息队列"的关键命令------消费者可以"挂起"等待新消息,有新消息就立刻处理,不会空轮询。

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 基本命令

bash 复制代码
# 添加一个或多个成员
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 支持两个集合之间的交集、并集、差集运算:

bash 复制代码
# 两个集合
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 判断是否允许

共同好友案例:

bash 复制代码
# 用户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 基本命令

bash 复制代码
# 添加元素(带分数)
ZADD zset1 100 a 200 b 50 c
# 添加三个元素:a(100分) b(200分) c(50分)

# 按分数升序查看
ZRANGE zset1 0 -1               # [c(50), a(100), b(200)]

# 按分数降序查看
ZREVRANGE zset1 0 -1            # [b(200), a(100), c(50)]

# 带分数返回
ZRANGE zset1 0 -1 WITHSCORES
# 返回 [c 50, a 100, b 200]

# 按分数范围查询(升序)
ZRANGEBYSCORE zset1 0 150       # [c(50), a(100)]

# 按分数范围查询(降序)
ZREVRANGEBYSCORE zset1 150 0    # [a(100), c(50)]

# 按排名查询(从0开始,升序)
ZRANGE zset1 0 2                # 排名0到2

# 按排名查询(降序)
ZREVRANGE zset1 0 2             # 最高分前3名

# 修改分数(累加)
ZINCRBY zset1 50 c              # c 的分数 50→100

# 获取元素的分数
ZSCORE zset1 a                  # 100

# 获取元素的排名(从0开始,升序)
ZRANK zset1 a                   # 1

# 获取排名(降序)
ZREVRANK zset1 a                # 1

# 统计分数范围内的元素数量
ZCOUNT zset1 0 150              # 2

# 查看元素数量
ZCARD zset1                     # 3

# 删除元素
ZREM zset1 c

# 按排名范围删除
ZREMRANGEBYRANK zset1 0 0       # 删除排名最低的

# 按分数范围删除
ZREMRANGEBYSCORE zset1 0 100    # 删除分数0~100的

3.3 ZSet 排行榜实战(面试核心)

场景:实现一个游戏实时排行榜

bash 复制代码
# 玩家注册/登录,加入排行榜
ZADD game:rank 0 "player:1001"
ZADD game:rank 0 "player:1002"
ZADD game:rank 0 "player:1003"

# 玩家获得分数(累加)
ZINCRBY game:rank 150 "player:1001"     # 1001 加 150 分
ZINCRBY game:rank 200 "player:1002"
ZINCRBY game:rank 100 "player:1003"

# 查看全球前10名(降序)
ZREVRANGE game:rank 0 9 WITHSCORES

# 查看某个玩家的分数
ZSCORE game:rank "player:1001"

# 查看某个玩家的排名(降序,第0名是第一名)
ZREVRANK game:rank "player:1001"

# 查看分数区间内的玩家
ZRANGEBYSCORE game:rank 100 300 WITHSCORES

# 清理低分玩家(比如删除分数<50的)
ZREMRANGEBYSCORE game:rank -inf 50

Java 代码示例:

java 复制代码
// 玩家加分
public void addScore(long userId, long score) {
    String key = "game:rank";
    redisTemplate.opsForZSet().incrementScore(key, "player:" + userId, score);
}

// 获取前N名
public Set<ZSetOperations.TypedTuple<String>> getTopN(long n) {
    return redisTemplate.opsForZSet().reverseRangeWithScores("game:rank", 0, n - 1);
}

// 获取某玩家排名
public Long getRank(long userId) {
    Long rank = redisTemplate.opsForZSet().reverseRank("game:rank", "player:" + userId);
    return rank == 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 性能分析

操作 时间复杂度
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 是面试的宠儿------不仅考命令,还考底层跳表和排行榜设计。

三个记忆锚点:

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

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

相关推荐
一铭111992 小时前
海豹办公 1.9.15 跨端办公软件实践 文档兼容 Access 数据库与移动 WebView 排错
android·数据库·electron·开源软件·access
Lynne3092 小时前
TSN 在变电站自动化通信网络升级中的应用价值与技术挑战
服务器·网络·数据库
弈栈录2 小时前
用户、会话与消息表应该如何设计
数据库·后端
内存漫游2 小时前
Checkpoint 与 Crash Recovery:数据库恢复机制入门
数据库
2301_786756603 小时前
中科院分区表停发后,期刊评价的技术逻辑:从期刊超越指数到多指标选刊方案
java·数据库·windows
闲云野鹤在人间3 小时前
Ubuntu 24.04 安装过程
服务器·数据库·ubuntu
lbb 小魔仙3 小时前
Redis运行状态总靠手动查?我用redis_exporter+Prometheus把指标、告警和远程抓取全接起来了
redis·bootstrap·prometheus
夕除3 小时前
redis--底层 sds inset
redis