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

redis数据结构和数据类型

1.动态字符串SIMPLE DYNAMIC STRING(SDS)

观察上图中的SDS结构,头部包含字符串长度和分配的空间,可以以O(1)的时间复杂度计算出字符串长度,并且有了字符串长度后可以无视c语言的字符串缺陷(\0作为结尾标识,容易被误判),可以动态扩容以及二进制安全。

2.intset

intset是一个集合,用于存储整形数据,由于是set所以满足元素唯一性,在不需要扩容的情况下,插入新元素时,底层采用二分法进行查找插入位置,效率为Ologn。

intset支持动态扩容,如果插入的元素大小大于当前编码ecoding设计大小,那么就会进行inset升级,具体见下述流程。

当插入一个比int16大得元素,那么就涉及到了整体的扩容,但这种情况会造成相当大得空间浪费。

具体过程是:假设目前的编码是int16,那么就升级为int32(如果int32能够装下新插入的数据,如果不够就继续升级),然后按照排列的倒叙对所有数据进行移动,最后再插入新元素。因为新加入的元素所需容量比剩余元素都大,并且所有元素都是int整形,那么新插入的元素一定是最大的,所以存储在末尾。

intSet 是一个set,本身得元素必须是唯一的,为了保证整体数据的有序性,所以在插入元素的时候需要进行判断:

判断方式即对传入数据的值进行查找,如果没有查找到,那么就插入,否则直接返回(有相同元素)。这个特性使得在当set集合中存储的元素是整数时,并且插入数据不超过set中默认设置的intset最大值时,intset作为set数据类型的数据结构,否则dict会作为set的数据结构。

查找的过程使用二分查找,根据intset存储的整形数据的value进行查找插入位置,用于保证intset的有序性。

3.dict

3.1Dict的数据结构

dict数据结构会创建一个dictht表示字典的hashtable,里面有一个指向entry数组的指针、size数组大小、sizemask使用与运算“&”进行取余、used表示已有entry的数量。

需要注意,这里的dictEntry是一个键值对。

后续的set就是使用dict进行实现的,不过将dictEntry设置为了0。

3.2Dict的扩容

dict这个数据结构存储了两张表,一张表用于存储所有的entry的指针,一张表用于rehash。

rehash的原因在于dict需要进行扩容,最初的大小为size,如果此时used(即已存储的entry数量)已经大于size的值了(即负载因子>1),那么此时就会考虑dict的扩容,前提是后台不忙碌的情况,当负载因子>5之后,dict不管后台是否有处理过程,都会进行扩容。

此时会根据used的数量,找到离used最近的2^n,比如现在的used为5,那么扩容的size就为8,会在等待执行rehash的表创建相应的存储容量,再将原本数据复制到新表中,最后将原表置空,从而完成扩容。

3.3Dict的收缩

如果负载因子<0.1,就会触发收缩,如果当前size<4那么会将另一张等待rehash的空表大小置为4,否则,根据size找到离size最近的2^n来进行扩容,最后将源数据复制到新表,将原数据表置为rehash辅助表。

3.4总结

dict的数据结构包含两张dictht表,记为ht(0)与ht(1),ht(0)用于存储当前数据,ht(1)用于rehash辅助来扩容与收缩。ht(0)与ht(1)会反复翻转,来实现扩容与收缩。扩容与收缩都是根据负载因子来判断的,最终都会找到离size最近的2^n作为新的存储容量。

最后需要了解一个细节,rehash不是一次性转移整张表,而是批量处理部分数据内容,因为数据量大的情况下,处理时间会很长

4.ZipList

4.1数据结构

ziplist的头部会保存整个链表的长度、尾指针以及entry的数量,ziplist用于节省存储空间

ziplist节省空间的原因在于不会存储指针,entry中会记录上一个节点的entry大小、entry中自身节点大小与编码方式以及自身数据,这样会比存指针和数据节省更多空间。这里的entry就是指一个节点。

同时entry中存储的内容content是可变长的,所以不会因为等长如intset浪费掉空间。

又由于ziplist中每个entry有上个节点的大小,所以可以通过地址计算进行顺序以及逆序遍历(顺序遍历通过当前entry起始地址加上本节点的大小计算下个节点的起始地址、逆序遍历通过当前entry的起始地址-上个节点大小)

4.2连锁更新问题

ziplist如果本节点的内容进行更新,假设现在使得后一个节点记录的前一个节点的长度从1字节变为了5字节,也就是说当前节点的更新 导致了 下个节点的更新(因为后一个节点记录了前一个节点的长度,而当前一个节点的长度大于254字节,就会使得后一个字节的前一个节点大小从1字节升级为5字节)。

在极端情况下就会出现一个节点更新,导致连锁反应,后续节点都进行更新,更新需要使用到内核态,涉及到内核态与用户态的切换,进行内存分配,资源开销很大。

5.quicklist

quicklist的创建就是为了解决ziplist的自身问题,比如ziplist需要连续的存储空间,那么当数据量很大时,内存分配很大的连续空间的难度会很高,所以为了解决这个问题,将ziplist拆分为多段,使用一个辅助节点来连接对应的ziplist段,每个辅助节点都有一个pre指针与next指针还有一个zip指针。

这个辅助接点就是quicklist节点。

整个数据结构称为quicklist。

quicklist可以对每个ziplist进行压缩,一般而言对中间节点进行压缩,因为查询是通过头节点与尾节点进行查询的。

6.SkipList

总结:跳表就是一个多指针链表,会在链表之间建立新的指针来增加遍历的跨度,根据score的大小来具体判断使用哪个跨度对应的指针,从而加速查找速度。

在redis中最多支持32级跳表

Redis 的跳表主要应用在 有序集合(ZSet) 的底层实现中(配合哈希表),它通过如下结构来维持元素的顺序

  • 每个元素按照 score(分数)从小到大排列
  • 插入时按照 score 插入合适的位置;
  • 查询时可以快速跳跃查找,比如范围查找、排名查找、按顺序遍历等。

7.RedisObject

8.String数据类型

string数据类型的基本编码方式是RAW,redisobject与SDS是分块存储的,而当SDS的大小小于44字节时,此时RAW编码会更改为EMBSTR。

如果存储的字符串是整数类型,并且在long范围以下,那么会在内存中创建一个空间用于存储整形数据,然后将指针指向该空间即可。这种编码方式是INT编码

9.list数据类型

list数据结构底层实现使用了quicklist,lpush和rpush对应双端队列得队首与队尾,lpop与rpop同理。

10.set数据类型

set结构底层实现使用的是dict数据结构,dict数据结构存储得是一个entry,也就是一个键值对。在set应用dict作为数据结构时,会将键值对中的值置为NULL

当存储的值都为整数,并且存储的数据量没有超过set中设置的最大值时,使用的数据结构是intset

,否则使用dict

11.zset数据类型

zset这个数据类型存储的内容是一个键值对,键存储对应的value,而值存储score。

zset的特性要求键值存储、唯一键、可排序的特性。

所以对应的数据结构,满足键值存储的就只有dict字典。但是dict实际上是一个hash表,无法进行排序在zset中进行键值查询

跳表本身包含score,在zset中用于范围查询和排序功能。

所以zset选择将数据分别存储在skipList和dict中,使用dict来进行查询键值对,使用skipList来进行排序。

当zset中的数据量较少,单个数据的大小也较小,具体小于某个预设值时,为了节省空间,zset会转换为一张ziplist压缩表,ziplist本身不支持排序功能,但是其占用空间小,并且存储的数据少,相应查询时间也不会太长。相当于是一种时间换空间的策略,但是由于数据量少,这个时间的增长可以接收。

ziplist存储的entry只能包含一个内容,所以在zset中,两个连续的entry会分别存储value和score

12.hash数据类型

底层就是使用dict,完全适配,没什么好说的。

13.redis数据结构以及对应的数据类型总结

redis数据结构有SDS,这是redis自带一种字符串存储结构,其避免了c语言的字符串问题,同时支持空间的动态扩充,能以O(1)时间复杂度读取字符串长度以及具有二进制安全;

有intset,这是一种相对灵活的整形数组,所有元素存储大小都是相同的,如果某个元素大于当前存储元素的最大空间,那么intset会进行动态升级。在intset中会在头部记录存储的数据数量、尾部指针、当前的数据存储空间大小(8、16、32)。支持存储空间动态扩张、插入有序、底层使用了二分法来插入数据,所以元素是有序的,读取速度快,缺点是需要连续的存储空间,并且伴随相当一部分的空间浪费;

有dict字典,这是一个典型的哈希表,通过hash code得到散列值,再将散列值与掩码进行与运算得到具体的存储位置。与经典哈希不同的点就在于使用与运算进行装填。dict包含两张ht,一张ht用于存放数据,一张ht用于rehash进行扩容与紧缩。扩容与紧缩都是根据负载因子来进行的,当负载因子大于1并且无后台操作时,或者负载因子大于5时,进行扩容,当负载因子<0.1时进行紧缩。扩容与紧缩都是找当前离size最近的2^n来作为新的size,比如原本size为5,现在就取8;

有zipList压缩链表,压缩链表特点是每个节点包含上一个节点的长度以及自身长度,可以看作是一个可变长的列表,虽然不支持随机访问,但是支持顺序、逆序访问。由于没有使用指针,并且每个数据内容都装满了存储空间(除了存储的辅助信息外),存储的密度非常高,基本不会出现空间浪费。缺点是需要申请连续的存储空间,当所需连续空间较大时,操作系统难以分配所需空间,同时还有可能出现连锁更新,即一个节点扩容,导致下个节点的encoding部分增大,刚好又引起下下一个节点的encoding部分增大,从而出现连锁反应;

quicklist快速链表,快速链表就是使用辅助接点将ziplist穿起来,并且辅助结点有pre behind、zip三个指针。quicklist解决了ziplist的问题,使得一个很长的数据内容能够切成多篇进行存储。quicklist是list数据类型的底层实现。同时quicklist能够将ziplist的中间部分进行压缩,因为list数据类型基本只会对队首与队尾进行操作,通过压缩中间部分内容可以节省存储空间;

skiplist跳表,跳表可以看作是一个多指针链表,链表中不同的节点间建立指针来加快查询速度,跳表的查询速度基本与红黑树相当。并且在跳表中维护了一个score值,根据score值来进行查询,所以跳表的数值是有序的。目前学到的数据结构中,就skiplist和intset是有序的,intset底层使用二分查找实现有序插入,而skiplist根据score来进行有序插入;

redisobject,所有redis数据存储的顶层封装就是redisobject,包含对应的数据编码方式,数据指针等。

在数据类型中学习了String、list、set、zset、hash

String使用SDS进行存储,没什么好说的;

list使用quicklist实现,quicklist通过多个辅助节点和多个ziplist组合而成,将中间ziplist进行压缩,留下首尾节点(可以调整首尾节点的数量),可见list结构不支持顺序检索、但是存储密度大、只需对首尾进行操作;

set集合底层使用dict字典数据结构,数据包含多个内容,比如Sadd text 01,02,03,这里就插入01、02、03三个数,显然01、02、03数据的结构不是一个键值对,所以使用dict时会保留键,而将值置空;

zset底层使用跳表和dict数据结构实现,跳表用于根据score进行范围查询,通过key查value(对应score)就需要使用到dict。同时需要注意zset的键唯一性是由dict实现的;

最后hash由dict实现没什么好说的。

.redis如何保证数据一致性

redis与数据库之间的一致性是极为重要的,但是数据的高一致性就意味着更大的性能开销:

1.先修改数据库再修改缓存,但这种方式很容易出现问题,比如某个线程再修改完数据库之后,再去修改缓存却因为未知原因挂掉了,此时没有后续干预,那么redis与数据库之间就出现了不一致问题。这种方法的一致性低,但是效率高,因为不需要进行重建缓存,如果一致性要求不高,可以使用该方法提升效率。

2.还有一种常见的作法就是,

写操作需要更新数据时:直接修改数据库,然后删除缓存,
而读操作如果缓存命中,那么直接返回缓存信息,如果未命中,那么就去读数据库进行缓存重建。

这种做法缓解了第一种方法的不一致情况,因为直接删除的开销会小于修改的开销,所以出错的概率会更小。

但是会增加缓存重建的开销,并且仍然可能出现问题。

3.延时双删,建立一个消息队列进行兜底,使用延时策略,在删除缓存之后的某个时间,比如200ms,来进行读缓存与数据库,如果数据一直,则不做操作,否则删除缓存。

延时双删仍然会存在短暂的数据不一致情况。

http://www.xdnf.cn/news/15330.html

相关文章:

  • vector之动态二维数组的底层
  • 2025年亚太中文赛B题第一版本超详细解题思路
  • C++:非类型模板参数,模板特化以及模板的分离编译
  • Java大厂面试故事:谢飞机的互联网医疗系统技术面试(Spring Boot、MyBatis、Kafka、Spring Security、AI等)
  • FastAPI + SQLAlchemy (异步版)连接数据库时,对数据进行加密
  • 【字节跳动】数据挖掘面试题0016:解释AUC的定义,它解决了什么问题,优缺点是什么,并说出工业界如何计算AUC。
  • UE5多人MOBA+GAS 18、用对象池来设置小兵的队伍的生成,为小兵设置一个目标从己方出生点攻打对方出生点,优化小兵的血条UI
  • (补充)RS422
  • 【每日刷题】x 的平方根
  • 2D下的几何变换(C#实现,持续更新)
  • Elasticsearch混合搜索深度解析(下):执行机制与完整流程
  • 【AI News | 20250710】每日AI进展
  • 2025年DevSecOps工具全景图:安全左移时代的国产化突围
  • 深入探索Kafka Streams:企业级实时数据处理实践指南
  • 11. TCP 滑动窗口、拥塞控制是什么,有什么区别
  • 8-day06预训练模型
  • 揭示张量分析的强大力量:高级研究的基础-AI云计算拓展核心内容
  • Django老年健康问诊系统 计算机毕业设计源码32407
  • 从就绪到终止:操作系统进程状态转换指南
  • 将手工建模模型(fbx、obj)转换为3dtiles的免费工具!
  • 上半年净利预增66%-97%,高增长的赛力斯该咋看?
  • 聊一聊在 Spring Boot 项目中自定义 Validation 注解
  • 牛客小白月赛119
  • 进程状态 + 进程优先级切换调度-进程概念(5)
  • 【C++篇】二叉树进阶(上篇):二叉搜索树
  • Qt中QGraphicsView类应用解析:构建高效2D图形界面的核心技术
  • 数据结构-顺序表
  • 【C语言网络编程】HTTP 客户端请求(域名解析过程)
  • Oracle字符类型详解:VARCHAR、VARCHAR2与CHAR的区别
  • Qt数据库编程详解:SQLite实战指南