带你看懂Redis中的数据结构-(SDS、List、Dict)

最近在写一个业务的,本来几个接口就完事了,但是因为元旦假期,PM、DE都请假了(羡慕),没法推进了,闲着也是闲着,索性再跟着书复习一遍Redis中的数据结构。

SDS(简单动态字符串)

图示

SDS是Redis重新设计的一种字符串,中文名是简单动态字符串,如上图所示,它会有几个属性:

  1. len: 记录SDS的实际已经使用的长度,也就是字符串中buf数组中使用的字节的数量。
  2. free: 记录buf数组中未使用字节的数量。
  3. buf: 字节数组,用来保存字符串。

之前以为SDS里面完全没用到C语言中的字符串呢,其实buf指向的还是C语言原生的字符串。

要点

SDS的用处

SDS用处很多,Redis中大部分字符串都是用SDS存的,比如一句 set name 'zhangsan' 这里的key(name)和value(zhangsan)都是用SDS进行存储。

SDS和C语言原生字符串的区别

SDS里面也是含有C语言原生字符串的(buf指向的那个部分),这样的好处是可以方便的使用C语言原生字符串的一些函数而不用自己重新写了(比如printf(%s,s->buf))。但是也可以总结一下二者的区别,也就是面试官常问的"Redis为什么要重新设计一个SDS?"。

  1. 取字符串长度的时间复杂度不同
  • SDS结构体中,有一个单独的len来记录字符串的长度,是常数级复杂度。
  • C语言并不会记录字符串的长度,想要获取时需要遍历整个字符串,对遇见的每个字符进行计数,直到遇到代表字符串结尾的空字符串('/0')为止,复杂度为O(N)。
  • 想想看其实是一种空间换时间的思想。
  1. 杜绝缓冲区溢出
  • 上图展示的就是使用C语言的strcat()函数导致字符串S1溢出到S2的部分了。
  • 而SDS在进行修改前会检查空间是否满足要求,如果不满足会自动将SDS的空间扩展至执行修改所需要的大小,然后再进行修改。
  • 为了避免频繁的发生字符串长度修改,SDS引入了free属性解除了字符串长度和底层数组长度之间的关联。
  • 如果对SDS进行修改之后,SDS的长度小于1MB,那么程序分配和len属性相同大小的未使用空间。2n+1(额外的一字节用于保存空字符)
  • 如果对SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间。n+1MB+1
  • 当需要缩短SDS时,不会立马回收空间,而是使用free来记录空闲的空间。
  1. 二进制安全
  • C语言字符串是二进制不安全的,所以不能用来存放音频、图片类型的数据。
  • SDS不用空字符串来判断字符串的结尾,用len来决定字符串的长短(buf数组中的长度),所以是二进制安全的。

List(链表)

图示

list是由一个一个的listNode构成的双向链表,其中head指向头节点,tail指向尾节点,len记录链表的长度,dup函数用于复制链表节点所保存的值,free函数用于释放链表节点所保存的值,match函数用于对比链表节点所保存的值和另一个输入值是否相等。

要点

  1. list是一个双端无环链表
  2. 它被广泛的用于列表键、发布订阅、慢查询、监视器等地方。

Dict(字典)

图示

字典用来存放键值对,每个键都是唯一的,作为一个常用的数据结构,C语言没有内置,所以Redis自己实现了。

  • 哈希表(dictht)中的table是一个数组(类型为dictEntry),每个dictEntry结构保存着一个键值对。size记录了哈希表的大小。used属性记录了哈希表目前已经有键值对的数量。sizemask属性的值总是=size-1,等价于取模运算。

  • dictEntry中的v属性可以是一个指针,或者是一个uint64_t整数或者int64_t整数。next属性是指向两一个dictEntry的指针,这个指针可以将多个哈希值相同的键值对链接在一起,解决哈希冲突的问题。

  • 字典(dict)结构体中有一个dictht的数组,大小为2,一般情况下只会使用ht[0],ht[1]只会在对ht[0]进行rehash的时候使用。rehashidx属性用来记录rehash进度,如果目前没有在进行rehash,那么它的值为-1.

要点

字典的用处

  • Redis中有一个字典用来代表数据库,比如set msg "hello world" 就是一个键为 msg 值为hello world的键值对,放在了字典表示的数据库中。
  • 字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,Redis就会使用字典作为哈希键的底层实现。

解决哈希冲突

  • Redis的哈希表使用拉链法来解决哈希冲突,每个哈希表的节点都有一个next指针,使得多个路由到同一个索引上的节点可以使用链表串起来。这里采用的是头插法(因为redis是单线程,也就不需要考虑java中成环的问题)

rehash

当哈希表中的键值对不断的增加或者减少的时候,查询需要对哈希表的大小进行相应的扩展或者收缩。

  • 1)为字典的ht[1]哈希表分配空间,这个大小取决于要执行的操作和ht[0]当前包含的键值对数量(ht[0].used)。如果执行的扩展操作,ht[1]的大小为第一个大于等于ht[0].used*2的2的幂;如果执行的是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的2的幂。

  • 2)将保存在ht[0]的所有键值对rehash到ht[1]上面。

  • 3)当ht[0]包含的所有键值对都迁移到ht[1]上之后,释放ht[0],将ht[1]设置为ht[0],并新创建一个空白哈希表,为下一次rehash做准备。

什么时候进行rehash?

  • 服务器目前没有正在执行BGSAVE或者BGREWRITEAOF,并且哈希表的复杂因子大于等于1.
  • 服务器目前正在执行BGSAVE或者BGREWRITEAOF,并且哈希表的负载因子大于等于5.
  • 负载因子 = ht[0].used/ht[0].size
  • 上面的是进行扩展的,当负载因子小于0.1的时候进行收缩。

渐进式rehash

  • rehash的动作不是一次性完成的,因为哈希表中的数据量可能是庞大的,如果一次性将所有的键值对都rehash到ht[1]的话,可能会导致服务器在一段时间内停止服务。所以需要分多次、渐进式的完成。
  • 1.一开始让字典同时持有ht[0]和ht[1]两个哈希表;2.将rehashidx由-1改为0表示rehash工作正式开始;3.在rehash进行期间,每次对字典执行添加、删除、查找、更新操作的时候,会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,并把rehashidx的值+1;4.随着字典操作的不断执行,最终在某个时间点上ht[0]的所有的键值对都会被rehash到ht[1]上,这时将rehashidx的值设为-1。
  • 在进行rehash期间,字典的删除、查找、更新操作会现在ht[0]进行,如果没找到才去ht[1]中找。而新添加操作则直接在ht[1]中进行。
相关推荐
乘风御浪云帆之上40 分钟前
数据库操作【JDBC & HIbernate & Mybatis】
数据库·mybatis·jdbc·hibernate
SomeB1oody1 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody1 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
dazhong20122 小时前
PLSQL 客户端连接 Oracle 数据库配置
数据库·oracle
啦啦右一3 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien3 小时前
Spring Boot常用注解
java·spring boot·后端
了一li4 小时前
Qt中的QProcess与Boost.Interprocess:实现多进程编程
服务器·数据库·qt
盛派网络小助手5 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
码农君莫笑5 小时前
信管通低代码信息管理系统应用平台
linux·数据库·windows·低代码·c#·.net·visual studio
∝请叫*我简单先生5 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl