当前位置: 首页 > news >正文

Redis常用数据结构以及多并发场景下的使用分析:Sorted List类型

文章目录

  • 前言
  • redis中的 Sorted List结构
    • 元素少: 压缩列表(ziplist)
    • 元素多: 跳跃表(skiplist) + 哈希表
      • 疑问1: 为什么时间复杂度是O(log n)?
      • 疑问2: 时间复杂度为O(log n)的数据结构 很多 什么使用跳表实现?
      • 疑问3: 跳表最高层=32 是什么意思?
      • 疑问4: 跳表如何维护高层稀疏 (起到索引的功能呢)
      • 疑问5: 为什么每次插入一个实际的值(底层),还要根据概率去更新高层的索引?
      • 疑问6: 生成 3 层 → 同时在 Level 1、2、3 出现,维护指针。 为什么需要同时 Level 123 都需要新插入 而不是只在level3?
    • ZSet 结构总结
    • Redis Sorted Set 操作汇总 + 跳表原理分析
    • 使用场景
      • 基于 Redis Sorted Set 的排行榜系统
      • Top-K 点赞排行榜
  • redis中Sort List 和hash 类型对比
  • 总结:

前言

我们已经分析了四种不同数据类型 在redis中的使用 以及底层的数据结构

Redis常用数据结构以及多并发场景下的使用分析:String类型
Redis常用数据结构以及多并发场景下的使用分析:Hash类型
Redis常用数据结构以及多并发场景下的使用分析:List类型
Redis常用数据结构以及多并发场景下的使用分析:Set类型
接下来 我们要来分析下 Sorted List的基本使用 这一节我会觉得比较难 因为涉及到skiplist 跳表的理解 以及基于跳表的结构 redis支持的操作 跟紧我 上车 !

redis中的 Sorted List结构

底层实现:
元素少: 压缩列表(ziplist)
元素多: 跳跃表(skiplist) + 哈希表

特点: 有序且唯一
每个元素关联score
支持范围查询

首先 分析每一个数据结构的时候 都要抓紧它的特点 有序 唯一 支持范围查询 我们首先来分析元素少的时候的基本实现

元素少: 压缩列表(ziplist)

为了在数据量小、结构简单时节省内存,Redis 在 ZSet 初始阶段使用压缩列表 ziplist 实现,而不是跳跃表 + 哈希表

触发升级的两个配置阈值:
Redis 会根据如下两个条件决定是否使用 ziplist:

zset-max-ziplist-entries 128      # 最大 entry 数
zset-max-ziplist-value   64       # 每个元素最大字节数

如果超过任一阈值,就会自动升级为:

跳跃表(用于排序、范围、分页)+
哈希表(用于快速查找 value → score)

同样为什么采用zipList 很好分析
也是为了 **【连续内存能利用缓存】**的特性 和 我在 list类型中的分析一样
Redis常用数据结构以及多并发场景下的使用分析:List类型

也就是说此时 插入删除 范围查找查找其实都是O(n) 【为什么呢? 顺序遍历所有元素】 但是由于元素很少 所以还是很快

最终zipList的样子是这样的

[score1][member1][score2][member2][score3][member3]...(升序排列)

元素多: 跳跃表(skiplist) + 哈希表

首先你要了解Sort List 维护了两个数据结构 跳跃表(skiplist) + 哈希表
插入一条数据(score,member)

哈希表快速根据 member 查找score O(1)
skiplist按 score 排序,支持范围查找 O(log n)
这种“双结构”保证了:
查找、更新快(通过 哈希表)
排序、范围查询快(通过跳表)

这是一个简单的理解 即为什么需要双结构

【哈希表的查找快 + 跳表的 排序 范围查询】组合 维护 Sorted List的【有序且唯一 支持范围查询 】

首先给出一个简单的插入过程
来了一个数据 (score,member)

dict 记录:member -> score

skiplist 插入:(score, member),按 score 排序,有序链表。

数据结构内容作用
dictmember → score 哈希表用于 O(1) 快速查找一个 member 的 score
skiplist(score, member) 有序跳表节点用于根据 score 排序、范围查找、Top-N 等操作

okokok 到了重头戏 什么是跳表? 首先建议大家去YT看具体的数据结构讲解 然后再来看我的文章。因为图像化的讲解更为直观。

为了 直接让你理解 我用GPT生成了一个数据插入过程:

跳表插入全过程演示(从空跳表开始) 我们设定:

最大层数:32

概率因子 p = 0.5(为了更清晰讲解,实际 Redis 中是 0.25)

要插入的数据:[10, 20, 30, 25, 40]

每次插入我们都用固定“随机层数”,方便你理解结构变化

Step 1:插入 10(生成层数 1)

Level 1:  ——> 10

10 是第一个节点,层数是 1,仅出现在最底层。

Step 2:插入 20(生成层数 2)

Level 2:         ——> 20
Level 1:  ——> 10 ——> 20

20 的层数为 2,出现在第 2 层和第 1 层。

Step 3:插入 30(生成层数 3)

Level 3:                   ——> 30
Level 2:         ——> 20 ——> 30
Level 1:  ——> 10 ——> 20 ——> 30

30 出现在 Level 3、2、1

Step 4:插入 25(生成层数 1)

Level 3:                   ——> 30
Level 2:         ——> 20 ——> 30
Level 1:  ——> 10 ——> 20 ——> 25 ——> 30

25 插入在 Level 1 中,介于 20 和 30 之间。

查找路径:

Level 3:20 < 25 → 不能走,降到 Level 2;

Level 2:20 < 25 → 不能走,降到 Level 1;

Level 1:20 → 插入在后面。

Step 5:插入 40(生成层数 1)

Level 3:                   ——> 30
Level 2:         ——> 20 ——> 30
Level 1:  ——> 10 ——> 20 ——> 25 ——> 30 ——> 40

40 插入在 Level 1 最末尾。
插入过程只更新 Level 1 指针。

最终跳表结构(简化版)

Level 3:                   ——> 30
Level 2:         ——> 20 ——> 30
Level 1:  ——> 10 ——> 20 ——> 25 ——> 30 ——> 40

越高层越稀疏 → 快速定位
越底层越完整 → 遍历所有元素

我这里写的不好 建议你去参考别的文章获得视频 去理解跳表
你可以想象为 最底层Level 1 是连续的数据

Level 1: ——> 10 ——> 20 ——> 25 ——> 30 ——> 40

然后上面的层数都是索引 最高层的索引很稀疏 然后每次要查找 \删除元素 都是从最高处的高层索引一路走到底层Level1 去获取数据

所以跳表的时间复杂度是O(log n)

疑问1: 为什么时间复杂度是O(log n)?

以查找为例:

从最顶层往右走,直到当前节点比目标值大(或到底)。

然后往下一层,继续向右找。

直到到达底层 Level 1,找到目标。

由于每层只需要“局部移动”,而总层数为 O(log n),所以总搜索路径也是 O(log n)。

疑问2: 时间复杂度为O(log n)的数据结构 很多 什么使用跳表实现?

特性跳表(SkipList)AVL / 红黑树等平衡树
插入/删除复杂度平均 O(log n)最坏 O(log n)(需旋转)
插入/删除实现简单,无需平衡调整复杂,需要重平衡/旋转等操作
多范围查询效率高效,天然有序支持区间较复杂,需中序遍历或递归
并发扩展性好(结构相对独立)较差(指针修改涉及大量旋转)
实现代码简单,基于随机索引复杂,要维持平衡性和颜色等属性
Redis 中表现插入/删除/范围查找快不适合 Redis 的高性能需求

跳表没有旋转、没有严格的“平衡要求”,但性能几乎不输红黑树,反而更好实现、更稳定。

疑问3: 跳表最高层=32 是什么意思?

高层的索引最多不会超过 32 层(由 Redis 限制)

疑问4: 跳表如何维护高层稀疏 (起到索引的功能呢)

假设我们现在要往跳表中插入一个值,比如 42,那么 Redis 会调用一个函数 randomLevel(),这个函数的作用就是:

用一定的概率(如 0.25)决定这个节点的层数
例如:

随机生成层数含义
Level 1最底层,必然存在,100% 概率
Level 2概率 25%,比 Level 1 少四分之三
Level 3概率 6.25%,更少
Level 4概率 1.56%
最多不会超过 32 层(由 Redis 限制)

也就是说,越高层数出现的概率越低,使得跳表的整体结构近似于“金字塔”,上面稀疏,底下密集。

疑问5: 为什么每次插入一个实际的值(底层),还要根据概率去更新高层的索引?

因为跳表本质上就是**“多层索引结构的链表”**。每插入一个节点,如果只插到底层,它的查找效率就和普通链表一样是
O(n),所以我们通过「概率方式提升索引层」来优化查找性能,使它达到 O(log n)。

随机生成层数(随机索引高度)
当插入一个新节点时,会根据概率(默认 Redis 为 p=0.25)随机生成这个节点在跳表中会出现的层级数。

例如:生成 1 层 → 只参与最底层;生成 3 层 → 同时在 Level 123 出现,维护指针。插入位置查找从跳表最高层开始查找合适位置;如果该节点出现在第 k 层,就在每一层维护一个“前驱节点”,并更新对应指针。结构更新低层插入的是数据节点;高层插入的是索引指针(shortcut),相当于对底层的加速导航。

疑问6: 生成 3 层 → 同时在 Level 1、2、3 出现,维护指针。 为什么需要同时 Level 123 都需要新插入 而不是只在level3?

因为:1. 跳表的高层依赖低层
跳表查找是从顶层逐层向下“跳”的。如果节点只出现在 Level 3,而不在 Level 2Level 1,那么当你从 Level 2 跳下来时,你根本找不到这个节点!所以为了保持层级间的一致性与可达性,节点必须从 Level 1Level k 连续存在。2. 保持“路径可达性”
跳表类似一个“电梯楼”:
Level 3:   ——> X
Level 2:   ——> X
Level 1:   ——> X
如果只让 X 出现在 Level 3,那你从 Level 2Level 1 根本“看不到”它,就不能访问它。所以它必须同时出现在 Level 123,形成“可逐层访问”的跳表路径。

ZSet 结构总结

Redis 中的 ZSet 是组合结构

dict<string, score>:快速查元素是否存在

skiplist<score, string>:支持有序范围查找

两者结合,既保证了:

O(1) 的单元素访问(dict)

O(log n) 的有序查询(skiplist)

Redis Sorted Set 操作汇总 + 跳表原理分析

Redis 命令操作类别跳表是否参与跳表的作用 / 结构维护时间复杂度
ZADD添加成员插入节点到 skiplist,并按 score 确定位置,同时也加入 dictO(log N)
ZREM删除成员删除 skiplist 中对应节点,同时从 dict 中移除O(log N)
ZSCORE查分数只查 dict(key→score 映射)O(1)
ZRANK排名查找在 skiplist 中累加 span 获取 rankO(log N)
ZREVRANK反向排名转换为 ZCARD - 1 - ZRANKO(log N)
ZRANGE范围查找按 rank 从 skiplist 中顺序访问节点O(log N + M)(M为结果数)
ZREVRANGE反向范围类似,但跳表只支持正向,所以倒序遍历前要倒排 rankO(log N + M)
ZINCRBY分数加减原节点删除后重新按新 score 插入跳表O(log N)
ZCARD元素总数直接读取 dict 长度O(1)
ZCOUNT分数范围从跳表中按 score 定位后遍历范围O(log N + M)
ZRANGEBYSCORE分数区间取值跳表从首个 ≥min 的 score 开始遍历直到 >maxO(log N + M)
ZREMRANGEBYSCORE删除区间跳表按 score 范围批量删除O(log N + M)
ZREMRANGEBYRANK删除排名区间按 rank 定位并遍历删除O(log N + M)

跳表与 Dict 的协作机制

Dict(哈希表):用于通过成员名快速定位 score(ZSCORE 用它)

SkipList(跳表):用于:

排序操作(如:排名、范围、区间删除)

排名计算(span)

范围遍历

二者同时维护,插入/删除时需同步操作。

使用场景

基于 Redis Sorted Set 的排行榜系统

排行榜结构在 Redis 中的存储

Key: leaderboard:game123
Type: Sorted Set (ZSet)
Data:user_100 → 999user_200 → 879user_300 → 861...内部维护:dict<String userId, double score> 做快速定位skiplist 做范围查询 & 有序排名支持
@Service
public class RedisSortedSetService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;private ZSetOperations<String, String> zSetOps() {return redisTemplate.opsForZSet();}// ===== 场景1: 排行榜系统 =====/*** 更新用户积分 - 支持并发*/public void updateUserScore(String leaderboard, String userId, double score) {String key = "leaderboard:" + leaderboard;// ZINCRBY 原子性增加分数zSetOps().incrementScore(key, userId, score);// 设置过期时间redisTemplate.expire(key, 30, TimeUnit.DAYS);System.out.println("用户 " + userId + " 在 " + leaderboard + " 增加了 " + score + " 分");}/*** 设置用户积分*/public void setUserScore(String leaderboard, String userId, double score) {String key = "leaderboard:" + leaderboard;zSetOps().add(key, userId, score);redisTemplate.expire(key, 30, TimeUnit.DAYS);}/*** 获取排行榜前N名 (从高到低)*/public Set<ZSetOperations.TypedTuple<String>> getTopRanking(String leaderboard, int count) {String key = "leaderboard:" + leaderboard;// ZREVRANGE - 从高分到低分return zSetOps().reverseRangeWithScores(key, 0, count - 1);}/*** 获取用户排名 (1-based)*/public Long getUserRank(String leaderboard, String userId) {String key = "leaderboard:" + leaderboard;// ZREVRANK - 倒序排名(分数从高到低)Long rank = zSetOps().reverseRank(key, userId);return rank != null ? rank + 1 : null; // 转换为1-based排名}/*** 获取用户分数*/public Double getUserScore(String leaderboard, String userId) {String key = "leaderboard:" + leaderboard;return zSetOps().score(key, userId);}/*** 获取指定分数范围的用户*/public Set<String> getUsersByScoreRange(String leaderboard, double min, double max) {String key = "leaderboard:" + leaderboard;return zSetOps().rangeByScore(key, min, max);}/*** 获取用户周围的排名*/public Set<ZSetOperations.TypedTuple<String>> getUserNeighborRanking(String leaderboard, String userId, int range) {String key = "leaderboard:" + leaderboard;Long rank = zSetOps().reverseRank(key, userId);if (rank == null) return Set.of();long start = Math.max(0, rank - range);long end = rank + range;return zSetOps().reverseRangeWithScores(key, start, end);}
}

Top-K 点赞排行榜

ZSet Key成员(value)分数(score)
article:like:zset文章 ID / 用户 ID / 视频 ID点赞数量(整数)

获取 Top-K 点赞最多的文章

Set<String> topArticles = redisTemplate.opsForZSet().reverseRange("article:like:zset", 0, K - 1);

对应 Redis 命令:

ZREVRANGE article:like:zset 0 k

redis中Sort List 和hash 类型对比

注意 两个背后都是hashtable 哟

ZSet = HashTable + SkipList,支持“排序 + 范围/排名操作”
Hash = HashTable only,仅支持“键值映射、高效点查”

数据结构差异:

项目ZSet(有序集合)Hash(哈希)
底层结构哈希表 + 跳表(skiplist)哈希表(dict)
是否有序按 score 有序无顺序
支持排名/TopK支持 ZRANK/ZREVRANGE 等不支持
范围查找(score)支持(ZREMRANGEBYSCORE)不支持
字段层级一个 key → 多个 member:value:score一个 key → 多个 field:value

常见使用场景对比:

场景推荐数据结构原因
排行榜 / Top-K 查询ZSet需要按 score 排序、分页、排名
点赞统计/积分记录ZSet用户、文章、积分等可排序统计
用户行为日志(按时间排序)ZSet用时间戳作为 score,可分页回查
用户点赞行为是否存在(快速判断)Hash查询效率高,结构清晰
用户-文章点对点点赞记录Hash按用户 ID 分桶,value 存文章 ID
Session、Token 信息存储Hash类似 Map,用途单一、易更新
配置数据、用户属性等KV结构Hash类似 JSON,对象属性读写方便

ZSet = 需要排序/Top-K 查询时用

Hash = 需要高并发读写、字段存储时用

ZSet 用于**“谁的点赞最多?”**

Hash 用于**“这个用户点赞了哪些文章?”**

总结:

业务场景是否推荐用 ZSet推荐理由
排行榜 Top-K强烈推荐自动排序,分页效率高
积分累计推荐支持原子加分,便于统计
点赞记录(总数统计)推荐用 score 累计点赞次数
用户是否点赞不推荐用 Hash / Set 更合适
用户点赞详情不推荐Hash or Set 更轻量
热门内容推荐推荐按权重实时排序取 Top-N
延时任务队列推荐分数做时间戳,高效到时拉取
用户行为日志推荐分数是时间戳,便于范围删查
秒杀活动不推荐不适合极高并发下写操作(写是 O(log n))
http://www.lryc.cn/news/582541.html

相关文章:

  • 算法设计与分析 知识总结
  • 【Python-GEE】如何利用Landsat时间序列影像通过调和回归方法提取农作物特征并进行分类
  • Paimon本地表查询引擎LocalTableQuery详解
  • DVWA靶场通关笔记-SQL盲注(SQL Injection Blind Medium级别)
  • 【Mac】实现Docker下载安装【正在逐步完善】
  • hmall学习
  • Apollo源码架构解析---附C++代码设计示例
  • 基于odoo17的设计模式详解---命令模式
  • 如何快速分析光伏电站气象数据?
  • 没合适的组合wheel包,就自行编译flash_attn吧
  • 云原生技术与应用-容器技术技术入门与Docker环境部署
  • 【RL+空战】学习记录01:jsbsim 仿真环境初次学习,F16 战机起飞
  • 吃透二分法的模板解法(适合所有类似于二分的算法题)
  • 【OceanBase 诊断调优】—— SQL 查询触发笛卡尔积怎么处理
  • Proface触摸屏编程软件介绍及下载
  • H3初识——入门介绍之常用中间件
  • vue前置知识-end
  • Vue 整合 Vue Flow:从零构建交互式流程图
  • 理解大模型智能体生态:从 Prompt 到 Agent 的完整信息流解析
  • LeetCode 1248.统计优美子数组
  • 【读代码】GLM-4.1V-Thinking:开源多模态推理模型的创新实践
  • 大模型面试:如何解决幻觉问题
  • 【python】pyserial 在windows 下卡住的bug
  • 在PPT的文本框中,解决一打字,英文双引号就变成中文了
  • 4.权重衰减(weight decay)
  • NumPy-随机数生成详解
  • 初识单例模式
  • 【网络安全】服务间身份认证与授权模式
  • 【Flutter】面试记录
  • Next.js 实战笔记 2.0:深入 App Router 高阶特性与布局解构