带你看懂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]中进行。
相关推荐
夜泉_ly1 小时前
MySQL -安装与初识
数据库·mysql
qq_529835352 小时前
对计算机中缓存的理解和使用Redis作为缓存
数据库·redis·缓存
月光水岸New5 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6755 小时前
数据库基础1
数据库
我爱松子鱼5 小时前
mysql之规则优化器RBO
数据库·mysql
闲猫5 小时前
go orm GORM
开发语言·后端·golang
丁卯4045 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo5 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser6 小时前
【SQL】多表查询案例
数据库·sql
Galeoto6 小时前
how to export a table in sqlite, and import into another
数据库·sqlite