目录

1. SDS 的定义

2. 动态字符串

常数复杂度获取字符串长度

杜绝缓存区溢出

SDS 两种优化策略

空间预分配

惰性空间释放

3. 链表和链表节点

每个链表节点用listNode结构表示

链表结构用list结构表示

Redis的链表实现的特性:

双端

无环

带表头指针和表尾指针

带链表长度计数器

多态

4. 字典的实现

4.1 字典结构

哈希表

哈希表节点

字典

4.2 哈希算法

4.3 哈希冲突

4.4 rehash

执行rehash步骤:

哈希表的扩展和收缩:

负载因子 = 哈希表已保存的节点数量/哈希表大小

渐进式rehash

5. 跳跃表

跳跃表节点

level数组

分值和成员

跳跃表结构

6. 整数集合

整数集合结构

扩容

7. 压缩列表

压缩列表的构成

压缩列表节点的构成

previou_entry_length

encoding

content

连锁更新

8. 对象

对象的类型和编码

类型

不同类型和编码的对象

字符串对象

int

raw

embstr

字符串命令

列表对象

ziplist

linkedlist​

列表命令

哈希对象

ziplist

hashtable

哈希命令

集合对象

intset

hashtable

集合命令

有序集合对象

ziplist

skiplist

有序集合命令

类型检查和命令多态

内存回收

对象共享

对象的空转时长


前言

本文基于 黄建宏-《Redis设计与实现》总结。第一部分为Redis 数据结构解析

1. SDS 的定义

struct sdshdr{int len; //buf数组中已用字节长度int free;//buf数组中未用字节长度char buf[]//buf数组
}

2. 动态字符串

常数复杂度获取字符串长度

O(1),相较于C语言的O(n)

杜绝缓存区溢出

字符串拼接时,SDS API先检查SDS空间,空间不足则扩展空间,再拼接

SDS 两种优化策略

空间预分配

对SDS进行空间扩展时,如果修改后的SDS的长度将小于1MB,则分配和len属性同样大小的未使用空间,即SDS的len属性和free属性相同

如果修改后的SDS的长度将大于1MB,则分配1MB的未使用空间

通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数

惰性空间释放

当SDS API需要缩短SDS保存的字符串时,程序不立即使用内存重分配来回收多出来的字节,而是使用free属性记录数量

C字符串和SDS之间的区别
C字符串 SDS 备注
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1) 因为SDS有属性len,而C字符串需要遍历直至找到空字符串
API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出 SDS API先检查空间,空间不足扩展后在拼接
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需要执行N次内存重分配 空间预分配策略
只能保存文本数据 可以保存文本或者二进制数据 C字符串除了末尾不能有空字符,因此不能八平村二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数 因为SDS遵循了C字符串以空字符结尾的惯例

3. 链表和链表节点

每个链表节点用listNode结构表示

typedef struct listNode {//前置节点struct listNode *prev;//后置节点struct listNode *next;//节点的值void *value;
}listNode;

链表结构用list结构表示

typedef struct list {//表头节点listNode *head;//表尾节点listNode *tail;//链表所包含的节点数量unsigned long len;//节点值复制函数void *(*dup)(void *ptr);//节点值释放函数void (*free)(void *ptr);//节点值对比函数int (*match)(void *ptr,void *key);
}list;

Redis的链表实现的特性:

双端

链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。

无环

表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。

带表头指针和表尾指针

通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。

带链表长度计数器

程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。

多态

链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

4. 字典的实现

4.1 字典结构

哈希表

typedef struct dichht {//哈希表数组dictEntry **table;//哈希表大小unsigned long siez;//哈希表大小掩码,用于计算索引值,总是等于size -1unsigned long sizemark;//该哈希表已有节点的数量unsigned long used;
}dictht;

哈希表节点

typedef struct dictEntry {//键void *key;//值union {void *val;uint64_tu64;int64_ts64;}v;//指向下个哈希表节点,形成链表struct dictEntry *next;
}dictEntry;

v属性保存键值对中的值,其中键值对的值可以是以一个指针,或者一个uint64_t整数,又或者是一个int64_t整数

字典

typedef struct dict {//类型特定函数dictType *type;//私有数据void *privdata;//哈希表dictht ht[2];//rehash索引,当rehash不在进行时,值为-1int rehashidx;
}dict;

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:

type属性是以恶搞只想dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数

privdata属性保存了需要传给那些类型特定函数的可选参数

ht属性是一个包含两项的数组,数组的每一项都是一个dictht哈希表,一般情况下,字典只用ht[0]哈希表,ht[1]哈希表只在对ht[0]哈希表进行rehash时使用

4.2 哈希算法

先根据键值对的key计算出hash和index,再根据index将该哈希表节点放到哈希表数组的指定index

hash = dict -> type -> hashFunction(key)
index = hash & dict ->ht[x].sizemark  (rehash 时 x= 1,没有rehash时 x =0)

当字典被用于数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值

4.3 哈希冲突

Redis的哈希表通过每个哈希表节点的next指针来连接索引相同的哈希表节点,采取头插法

4.4 rehash

随着操作的不断执行,哈希表保存的键值对会增多或减少,为了保证哈希表的负载因子在一个合理的范围内,会通过rehash来对哈希表扩展或收缩

执行rehash步骤:

1)给字典的ht[1]哈希表分配空间:

扩展时,ht[1]的大小为第一个大于等于2*ht[0].used的2的n次幂

收缩时,ht[1]的大小为第一个大于等于ht[0].used的2的n次幂

2)将ht[0]中的所有键值对rehash到ht[1]上:重新计算key的hash和index,然后放到ht[1]的指定index上

3)当迁移完成后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新建一个空白哈希表,为下一次rehash做准备

哈希表的扩展和收缩:

扩展:

1)服务器目前没有执行BGSAVE或BGREWRITEAOF命令,且哈希表的负载因子大于等于1

2)服务器目前在执行BGSAVE或BGREWRITEAOF命令,且哈希表的负载因子大于等于5

在执行BGSAVE或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器提高执行扩展操作所需的负载因子,避免在子进程存在期间进行扩展操作,避免不必要的内存写入操作

收缩:

当哈希表的负载因子小于0.1时,执行收缩操作

负载因子 = 哈希表已保存的节点数量/哈希表大小

load_factor = ht[0].used / ht[0].size

渐进式rehash

扩展或收缩哈希表需要将ht[0]的所有键值对rehash到ht[1]里面,但是这个rehash时分多次,渐进性完成的

因为如果键值对数量较大,一次性rehash,庞大的计算量可能会导致服务器在一段时间内停止服务

渐进式rehash将reahahs键值对所需的计算工作均摊到对字典的每个增删改查操作上。在渐进式rehash过程中,字典会同时使用ht[0]和ht[1]两个哈希表,删除,查找,更新操作会在两个哈希表上进行,增加才做只会保存到ht[1]里面

5. 跳跃表

跳跃表是有序集合的底层实现之一

跳跃表节点

typedef struct zskiplistNode {//层struct zskiplistLevel {//前进指针--用于访问位于表尾方向的其他节点struct zskiplistNode *forward;//跨度 --前进指针所指向节点与当前节点的距离unsigned int span;}level[];//后退指针 --从表尾方向往表头方向遍历时使用,一次只能遍历一个节点struct zskiplistNode *backward;//分值double score;//成员对象robj *obj;
}zskiplistNode;

level数组

跳跃表节点的level数组的每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度越快

每次创建新跳跃表节点的时候,程序都根据幂次定律(越大的数出现的概率越小)随机生成一个介于1-32之间的值作为level数组的大小

分值和成员

跳跃表中的所有节点都按分值从小到大来排序,如果分值相等,则根据成员对象在字典序中顺序排序

obj属性是一个指针,指向一个字符串对象,字符串对象保存着一个SDS值

跳跃表结构

typedef struct zskiplist {//表头节点和表尾节点struct zskiplistNode *header,*tail;//表中节点的数量unsigned long length;//表中层数最大的节点的层数int level;
}zskiplist;

6. 整数集合

整数集合intset是集合键的底层实现之一,当一个集合只包含整数元素且元素数量不多,Redis就有使用整数集合作为集合键的底层实现。整数集合是有序且不重复的。

整数集合结构

typedef struct intset {//编码方式uint32_t encoding;//集合包含的元素数量uint32_t length;//保存元素的数组int8_t contents[];
}intset;

contents数组的真正类型取决于encoding属性的值,数组的长度=数组中元素的个数*sizeof(数组的类型)。例如encoding属性的值为INTSET_ENC_INT16,contents数组保存着5个元素。则数组的长度等于5*sizeof(int16_t) = 5*16=80位

扩容

根据新元素的类型,扩展底层数组的空间大小,并为新元素分配空间‘

将现有的所有元素转为与新元素相同的类型,并放在正确的位置上,保持有序不变

将新元素添加到底层数组中

7. 压缩列表

压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量的列表项,且每个列表项是小整数值或者短字符串;当一个哈希键只包含少量键值对,且每个键值对的键和值是小整数值或者短字符串,那么Redis会使用压缩列表作为底层实现。

压缩列表的构成

压缩列表节点的构成

previou_entry_length

记录了前一个节点的长度。如果有一个指向当前节点起始地址的指针C,用C减去当前节点previou_entry_length属性的值,可以得到指向前一个节点起始地址的指针P.压缩列表从表尾向表头遍历就是通过这个原理实现的

encoding

记录了节点的content属性所保存数据的类型和长度

content

保存节点的值,可以是一个字节数组或者整数,值的类型和长度由encoding属性决定

连锁更新

最坏情况下需要对压缩列表执行N次空间重分配操作,每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N*2)

8. 对象

对象的类型和编码

Redis使用对象来表示数据库中的键和值,每当在Redis创建一个键值对时,至少会创建两个对象,一个键对象,一个值对象。每个对象都由redisObject结构表示,与保存数据相关的三个属性分别是type,encoding,ptr。

typedef struct redisObject {//类型unsigned type:4;//编码unsigned encoding:4;//指向底层实现数据结构的指针void *ptr;//...
}rojb;

类型

对于Redis保存的键值对,键总是一个字符串对象,而值可以是字符串对象,列表对象,哈希对象,集合对象或者有序集合对象其中一个

不同类型和编码的对象

字符串对象

字符串对象的编码可以可以是int,raw或者embstr。

int

raw

embstr

raw编码会调用两次内存分配函数来创建redisObject和sdshrd结构,而embstr调用一次内存分配函数来分配一块连续的空间,包含redisObject和sdshdr结构。

字符串命令

列表对象

列表对象的编码可以是ziplist或者linkedlist。

ziplist

linkedlist

列表命令

哈希对象

哈希对象的编码可以是ziplist或者hashtable

ziplist

hashtable

哈希命令

集合对象

集合对象的编码可以是intset或者hashtable

intset

hashtable

集合命令

有序集合对象

有序集合对象的编码可以是ziplist或者skiplist

ziplist

skiplist

skiplist编码的有序集合对象使用zet结构作为底层实现,一个zset结构包含一个字典和一个跳跃表.

执行范围型操作ZRANK,ZRANGE用跳跃表,执行分值查询用字典

typedef struct zset {zskiplist *zsl;dict *dict;
}zset;

有序集合命令

类型检查和命令多态

Redis用于操作键的命令基本分为两种类型,一种可以对任何类型的键执行,例如DEL,EXPIRE,一种只能对特定类型的键执行,例如SET,GET只能操作字符串键,HDEL,HSET只能操作哈希键。执行命令前都会通过redisObject的type属性进行类型检查。

DEL,EXPIRE 和 LLEN等的区别在于,前者是基于类型的多态,即一个命令可以同事用于处理多种不同类型的键;而后者是基于编码的多态,即一个命令可以同时用于处理多种不同的编码。

内存回收

Redis构建了一个引用计数实现的内存回收机制。创建一个新对象时,引用计数的值被初始化为1,被新程序使用时,引用计数值加一,不再被一个程序使用时,引用计数值减一,引用计数值变为0时,内存被释放

对象共享

Redis中,让多个键共享同一个值对象需要执行两个步骤:

将数据库键的值指针指向一个现有的值对象

将被共享的值对象的引用计数加一

对象的空转时长

redisObject结构还包含一个lru属性,该属性记录了对象最后一次被命令程序访问的时间。

OBJECT IDLETIME命令可以打印给定键的空转时长,通过当前时间 - lru时间计算得出。

当回收内存算法为volatile-lru或者allkeys-lru,那么当服务器内存超过了maxmemory时,优先回收空转时长较高的键。

Redis(一)数据结构解析相关推荐

  1. redis常用数据结构解析

    Redis是一个开源的Key-Value存储引擎,它支持string.hash.list.set和sorted set等多种值类型.由于其卓越的性能表现.丰富的数据类型及稳定性,广泛用于各种需要k/v ...

  2. Redis基本数据类型String——数据结构解析

    String Redis没有直接使用C语言的传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型. 下面我将解释为什么Redis要自己 ...

  3. 硬核资源!Redis 五种数据结构以及三种高级数据结构解析(详解)

    上一篇分享的是<深入理解JVM>,这篇给大家分享<Redis 五种数据结构以及三种高级数据结构解析>. 前言 在 Redis 最重要最基础就属 它丰富的数据结构了,Redis ...

  4. Redis 五种数据结构以及三种高级数据结构解析以及使用

    一.前言 在 Redis 最重要最基础就属 它丰富的数据结构了,Redis 之所以能脱颖而出很大原因是他数据结构丰富,可以支持多种场景.并且 Redis 的数据结构实现以及应用场景在面试中是相当常见的 ...

  5. Redis高级数据结构原理解析-bitmap,hyperloglog

    Redis 位图 开发过程中,我们可能遇到这种场景记录用户的打卡情况,签到情况,这些场景只有两种结果,有或者没有,加入记录的数据量比较大,比如用一年的数据,如果用Redis中普通key/value,每 ...

  6. 将一个键值对添加入一个对象_细品Redis高性能数据结构之hash对象

    背景 上一节讲Redis的高性能字符串结构SDS,今天我们来看一下redis的hash对象. Hash对象 简介 redis的hash对象有两种编码(底层实现)方式,字典编码和压缩列表编码.在使用字典 ...

  7. [转]Redis内部数据结构详解-sds

    本文是<Redis内部数据结构详解>系列的第二篇,讲述Redis中使用最多的一个基础数据结构:sds. 不管在哪门编程语言当中,字符串都几乎是使用最多的数据结构.sds正是在Redis中被 ...

  8. Redis——底层数据结构原理

    摘要 Redis 发展到现在已经有 9 种数据类型了,其中最基础.最常用的数据类型有 5 种,它们分别是:字符串类型.列表类型.哈希表类型.集合类型.有序集合类型,而在这 5 种数据类型中最常用的是字 ...

  9. Redis底层数据结构详解(一)

    Redis底层数据结构 一.简单动态字符串SDS 1. SDS 2. 为什么Redis没用C语言原生字符串? 2.1 C语言中的字符串 2.2 使用SDS的好处 二.链表linkedlist 三.压缩 ...

最新文章

  1. linux的mount(挂载)命令详解
  2. 利用集群技术实现Web服务器的负载均衡
  3. DropDownList设置选定项,设置选择项,最安全的方法
  4. springboot 之Spring Web Mvc Framework
  5. STP生成树的选举详细步骤、四个案列详解(附图,建议电脑观看)
  6. Python安装第三方库的3种方法
  7. 项目经理的五大核心技能
  8. Android:android2.3电话接听
  9. python 3d大数据可视化软件_4个最受欢迎的大数据可视化工具
  10. 高斯克吕格投影换带计算(高斯正反算公式)
  11. RISC-V MCU 应用教程之RTC自动唤醒
  12. 经典卷积网络--ResNet残差网络
  13. sketch-矢量绘图应用软件
  14. pdffactory 打印字体_PdfFactory(虚拟打印机)
  15. 华硕主板H81M-E BIOS刷NVMe支持M.2固态硬盘成功
  16. Linux服务器 - 腾讯云服务器挂载云硬盘
  17. Java基础【08】常用API——RandomAccessFile相关API
  18. 《ANSYS 14.0超级学习手册》一1.1 有限元法概述
  19. python通过指定网卡发包_Python选择网卡发包及接收数据包
  20. Oracle APEX初体验

热门文章

  1. 对话 CTO | 听掌门教育 CTO 李海坚讲教育公平背后的技术价值
  2. 由一次线上故障来理解下 TCP 三握、四挥 Java 堆栈分析到源码的探秘
  3. 小米互联网思维新思考20141220
  4. ACC算法学习笔记(六):ASPICE开发流程
  5. 微信小程序 java校园跑腿服务平台uniapp
  6. vivo一面翻车,整理完这份Java面经分类汇总,我突然悟了
  7. 怎么通过手机+电脑在互联网上面赚钱
  8. RK3568 Android11从入门到实战项目专栏目录及介绍
  9. 导航地图是怎样绘制出来的?
  10. Sharepoint visio Web Access