带你看懂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]中进行。
相关推荐
2401_857622666 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
vvvae12347 小时前
分布式数据库
数据库
哎呦没7 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
雪域迷影7 小时前
PostgreSQL Docker Error – 5432: 地址已被占用
数据库·docker·postgresql
bug菌¹8 小时前
滚雪球学Oracle[4.2讲]:PL/SQL基础语法
数据库·oracle
逸巽散人8 小时前
SQL基础教程
数据库·sql·oracle
月空MoonSky9 小时前
Oracle中TRUNC()函数详解
数据库·sql·oracle
momo小菜pa9 小时前
【MySQL 06】表的增删查改
数据库·mysql