回顾Redis之对象与数据结构

引言

Redis是一款基于键值对的数据结构存储系统,它的特点是基于内存操作、单线程处理命令、IO多路复用模型处理网络请求、键值对存储与简单丰富的数据结构等等

这篇文章主要围绕Redis中的对象与数据结构来详细说明键值对存储与简单丰富的数据结构这两大特点

Redis中的数据以Key,Value键值对的形式存储在字典中,字典的实现是哈希表

键Key只能使用字符串对象来表示,值Value能够使用其他所有对象

对象与数据结构

Redis中存在丰富的对象,常用的对象(数据类型)有字符串对象string、列表对象list、散列对象hash、集合对象set、有序集合对象zset等

还有其他的数据类型如Bitmap、Hyperloglog、Geospatial、布隆过滤器等,但这篇文章只涉及常用的对象,其他数据类型再以后的文章中再展开说明

  • redis中的对象RedisObject由类型、编码、引用次数、lru、指向编码使用的数据结构对象构成

  • 类型标识这个对象是什么类型对象

  • 比如字符串、列表、哈希、集合、有序集合等

  • 编码表示构成对应类型对象时使用哪种数据结构

  • 引用次数表示这个对象被引用了多少次

  • redis内存回收使用引用计数法,回收引用次数为0的对象 redis只依赖字符串对象,而不存在循环依赖所以不存在循环引用,因此可以使用引用计数法

  • lru记录这个对象最近被调用的时间,当空间回收算法使用lru时会优先回收很久未用的对象(后续删除回收的文章会介绍)

数据结构

sds简单动态字符串

sds使用字节数组维护,len记录字符串长度(表示结尾的'\0'不算),free表示字节数组中空闲的长度

​在添加元素前会判断数组长度是否足够,不够则会进行扩容;扩容有空间预分配策略,会留有一部分空闲空间

如果下次修改字符串未超出数组长度就能够直接修改,节省了扩容的开销

hashtable字典

字典使用哈希表实现,哈希表的原理本篇文章不会详细概述

哈希冲突使用链地址法解决,查找时先通过 hash%数组长度-1 来获取索引,得到索引后再遍历链表节点,如果是新增则直接使用头插法,插入链表头部

​为了防止大字典扩容时发生阻塞,字典中哈希表的扩容是循序渐进的,在发生扩容时会有俩个哈希表

​旧哈希表和新哈希表中都可能存储数据,再收到hget等请求时先在旧哈希表中查找,找到了就顺便把它迁移到新哈希表中;在旧哈希表中没找到就去新哈希表中找

在完成迁移时,新哈希表将旧哈希表替换

skiplist跳表

跳表维护多层级的有序链表,利用高层能够快速达到后续节点,实现简单,维护方便,增删改查时间复杂度平均log n

​比如查找值为2.0的节点,查找顺序为图中虚线

先找到虚拟头节点,从当前维护的最高层(L5)开始寻找,往后找到o3对象值为3.0,说明已经找过头了,于是要去下一层进行寻找;来到L4先后遍历,o1对象值为1.0,比目标值2.0小,说明没有目标值在o1对象后面,于是来到o1对象L4层;继续在o1对象L4向后遍历,发现o3值为3.0大于目标值,于是降层来到o1对象L3层;L3层后面也是o3于是继续降层,来到L2层,L2层向后遍历为o2对象,值为2.0并比较o2对象相同说明找到了

从维护的最高层开始查询,查询为空或者查询值大于目标值则降层,当前在最后一层还需要降层说明找不到

当排序值相同时,按照对象大小排序,这里的对象都是字符串对象

增加节点时的层数是随机生成的,越高层几率越小;其他修改操作,也是通过查询再进行,同时还要维护一些如最高层级等其他属性

intset整数集合

intset 维护了一个有序,无重复的数组

在实现上使用数组、长度(记录元素数量)和编码(编码能够标识元素类型,如16、32、64位的整型)

​当加入的元素为当前数组内不存在的高位整型时(比如数组中都是32位整型,此时加入一个64位整型)发生升级:先申请内存重分配,再将旧元素移动到对应位置上,然后加入新元素同时修改编码,当删除高位整型时不会发生降级

intset的升级有效的节约内存,当set对象都为整型且数据量较小时使用intset实现以此来节约内存

ziplist压缩列表

ziplist用连续空间的节点构成,节点由记录前驱节点偏移量(逆序遍历)、编码(字节数组或整型的编码)、内容(内容可以是字节数组或整型)组成

​因为ziplist的内容不是固定的,比如记录前驱节点偏移量是可变长的,这会影响节点的长度,又因为ziplist是空间连续的,这会导致后续的节点空间都要变动,被称为连锁更新(发生的概率小)

为了节省空间,用于数量量小场景下列表、哈希、有序集合的实现

quicklist快速列表

快速列表可以当作双向链表,只不过节点使用ziplist,常用来实现数据量大场景下的列表对象

对象

说明:

下文中数据量代表着占用字节情况和数据元素数量

本篇文章不介绍各个对象的命令使用规则,需要学习命令的同学可以去官网查看

字符串对象

字符串对象string由sds简单动态字符串来实现

  • sds有不同的编码:int、embstr、row

  • int 用来存储整型字符串,计算时可能发生整型与字符串的转换

  • embstr 用来存储短的字符串,只分配一次内存,分配内存时同时分配redisobject和sds

  • row 用来存储长字符串,分配内存时需要分配两次:redisobject、sds

字符串对象是Redis中最常用的对象,也是唯一会被其他对象依赖使用的对象

字符串对象常见的使用场景:整存整取的缓存、计数器、分布式锁

列表对象

列表对象list是一个队列,可以操作队头队尾,由ziplist或quicklist来实现

数据量小时使用ziplist,数据量大时使用quicklist( linkedlist+ziplist )

列表的使用场景是FIFO队列保证元素访问顺序

哈希对象

哈希对象hash是维护KV键值对的无序数据结构,由ziplist或hashtable来实现

数据量少使用ziplist、数据量大使用hashtable

哈希的使用场景是缓存的部分存取(比如一个大礼包下有A商品B商品等)

集合对象

集合对象set的特点是无序、无重,由intset或hashtable来实现

数据量少且数据为整型使用intset、数据量大或数据不为整型使用hashtable且值永远为null

集合的使用场景是唯一性元素或交集并集(共同关注、可能认识)等(无序、无重复)

有序集合对象

有序集合对象zset是有序、无重的数据结构,由ziplist或skiplist + hashtable实现

数据量少时使用ziplist、数据量大时使用skiplist + hashtable(为了满足根据对象查询分指常量级功能,共享对象,不造成内存开销)

有序集合的使用场景是排行榜、关注程度榜单等(有序、无重复)

总结

本篇文章围绕Redis以键值对存储、丰富多元的数据结构为特点详细介绍了Redis中的对象与数据结构

对象由类型、编码、数据结构指针等构成

为了节省空间,每种类型的对象都有多种编码类型的数据结构能够实现

  • 字符串对象常用来做缓存、分布式锁、计数器等,被其他对象依赖使用

  • 由sds实现主要有int、embstr、row三种编码来处理不同类型的字符串,embstr处理短字符串优化内存分配

  • sds是动态字符串,利用空间预分配策略在修改不超过数组长度情况下可以不需要进行扩容,节省开销

  • 列表对象常用来维护队列元素有序性

  • 当数据量小时使用压缩列表ziplist实现,数据量大时使用快速列表quicklist实现

  • 压缩列表使用连续空间,节点中存储可以时字符串也可以是整型

  • 快速列表则可以当作链表,节点为压缩列表

  • 哈希对象常用来维护部分存取的缓存

  • 当数据量小时使用压缩列表zpilist实现,数据量大时使用哈希表hashtable实现

  • 哈希表为了防止阻塞,在扩容时使用新旧两个哈希表存储元素,在处理命令的同时完成迁移

  • 集合对象有无序、无重的特点,常用来做唯一、交集(共同好友)、并集(可能认识)

  • 当数据量小且元素都为整型时使用整型集合intset实现,当数据量大使用哈希表实现

  • 整型集合有不同的编码形式,充分节省了空间;使用哈希表时Value为空

  • 有序集合对象有有序、无重的特点,常用来做排行榜

  • 当数据量小时使用压缩列表实现;当数据量大时使用跳表skiplist+哈希表实现,哈希表保存K对象V比较值

  • 跳表是多层级有序的链表,平均时间复杂度在log n,简单易维护

<think>嗯,用户之前问了一个关于Java集合的问题,现在又问Redis的ZSET数据结构。我需要先回顾一下之前的对话,看看是否有联系。之前的回答解释了为什么修改Integer列表元素不生效,涉及到了不可变对象和集合操作的问题。现在用户转向Redis数据结构,可能是在学习不同的存储结构,或者在做项目时需要考虑数据存储方式。 首先,用户可能对Redis的基础有一定了解,但想深入理解ZSET。需要先介绍ZSET是什么,它的特点,比如有序、唯一成员、分数排序等。然后要解释底层实现,比如跳跃表和哈希表的结合,这样用户能明白为什么ZSET高效。 接下来,用户可能需要知道ZSET的常用命令,比如ZADD、ZRANGE、ZREVRANGE等,这些是操作ZSET的基本命令。给出示例能帮助用户更好地理解如何使用。另外,应用场景也很重要,用户可能想知道在实际开发中哪里会用到ZSET,比如排行榜、延时队列、范围查询等,这部分需要结合实际例子说明。 还要注意用户可能的进阶问题,比如为什么选择跳跃表和哈希表,或者ZSET的时间复杂度,这样可以在解释时提前涵盖这些点。此外,用户可能对ZSET和其他数据结构的区别感兴趣,比如和SET的区别,或者和列表的区别,可以简要提及。 最后,确保语言通俗易懂,避免过多技术术语,但又要准确。可能需要检查是否有错误,比如ZSET是否真的使用两种数据结构,或者命令的正确写法。另外,用户之前的问题涉及到修改集合元素,这里可以提一下ZSET的更新操作是否直接,比如更新分数是否容易,这也是实际应用中的常见需求。 总结下来,回答的结构应该是先定义ZSET,然后底层结构,常用命令,应用场景,最后注意事项。这样用户能逐步深入了解,并且能应用到实际项目中。同时,要确保信息准确,示例正确,帮助用户形成清晰的理解。</think>Redis 的 **ZSET**(有序集合,Sorted Set)是一种结合了 **Set**(成员唯一)和 **Sorted**(按分数排序)特性的数据结构。它通过为每个成员分配一个 `score`(分数值)来实现排序,是 Redis 中非常实用且高效的数据结构之一。 --- ### **核心特性** 1. **唯一成员,可重复分数** - 每个成员(`member`)唯一,但多个成员可以拥有相同的 `score`。 - 例如:`ZADD rank 90 "Alice" 90 "Bob"`,允许两人分数相同。 2. **按分数排序** - 默认按 `score` **升序**排列,支持范围查询(如获取排名前 N 的成员)。 - 可以通过 `ZREVRANGE` 实现降序排序。 3. **高性能操作** - 插入、删除、查询的时间复杂度为 **O(log N)**,基于底层**跳跃表(Skip List)**实现。 --- ### **底层实现** ZSET 内部使用两种结构组合实现: 1. **跳跃表(Skip List)** - 支持快速的范围查询(如 `ZRANGE`)和排序。 2. **哈希表(Hash Table)** - 以 `member` 为键,`score` 为值,实现 **O(1)** 复杂度的单个成员查询(如 `ZSCORE`)。 这种组合兼顾了**范围操作的高效性**和**单成员查询的快速性**。 --- ### **常用命令** | 命令 | 作用 | 示例 | |------|------|------| | `ZADD` | 添加成员 | `ZADD key score member [score2 member2 ...]` | | `ZRANGE` | 按升序返回成员 | `ZRANGE key start stop [WITHSCORES]` | | `ZREVRANGE` | 按降序返回成员 | `ZREVRANGE key start stop [WITHSCORES]` | | `ZRANK` | 获取成员升序排名 | `ZRANK key member` | | `ZSCORE` | 获取成员分数 | `ZSCORE key member` | | `ZREM` | 删除成员 | `ZREM key member` | | `ZCOUNT` | 统计分数区间内的成员数 | `ZCOUNT key min_score max_score` | --- ### **典型应用场景** 1. **排行榜** - 例如游戏积分排名,通过 `ZREVRANGE` 直接获取 Top N 玩家。 2. **延时队列** - 将任务到期时间作为 `score`,用 `ZRANGEBYSCORE` 轮询到期任务。 3. **范围查询** - 如按时间范围筛选数据(将时间戳作为 `score`)。 4. **权重调度** - 用 `score` 表示优先级,按权重顺序处理任务。 --- ### **示例:实现排行榜** ```bash # 添加玩家分数 ZADD rank 95 "Alice" 88 "Bob" 100 "Charlie" # 获取前2名(降序) ZREVRANGE rank 0 1 WITHSCORES # 输出:1) "Charlie" 2) "100" 3) "Alice" 4) "95" # 更新 Alice 的分数(直接覆盖) ZADD rank 99 "Alice" # 查询 Alice 的排名(升序排名从0开始) ZRANK rank "Alice" # 返回1(Bob=88, Alice=99, Charlie=100) ``` --- ### **注意事项** 1. **分数范围** - `score` 是双精度浮点数,但 Redis 实际存储为 64 位整数(可能导致精度丢失)。 2. **性能** - 超大 ZSET(如百万级成员)的批量操作可能较慢,需合理分片。 3. **内存优化** - 若仅需排序,可用 `ZSET`;若需要更复杂操作,可能需结合其他数据结构(如 Hash)。 ZSET 凭借其排序能力和高效操作,成为 Redis 中解决**有序且唯一**场景的利器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值