一. 引言

  《Redis设计与实现》一书主要分为四个部分,其中第一个部分主要讲的是Redis的底层数据结构与对象的相关知识。

  Redis是一种基于C语言编写的非关系型数据库,它的五种基本对象类型分别为:STRING,LIST,SET,HASH,ZSET。然而,对于每一种基本对象数据类型,底层都至少有2种不同的实现方式。

二. 简单动态字符串(Simple Dynamic String, SDS)

  SDS是Redis的默认字符串表示,包含字符串值的键值对底层都是由SDS实现的。除了保存数据库中的字符串值之外,SDS还被用作缓冲区。

示例:

redis>SET msg "hello world"
OK

  当执行上述代码之后,Redis会创建一个STRING类型的键值对,其中键和值均是一个字符串对象,键对象的底层是一个保存着字符串"msg"的SDS,而值对象的底层是一个保存着字符串"hello world"的SDS。

  每个SDS都结构如下所示

struct sdshdr{
//记录buf数组中已使用的字节数量(也是SDS所保存的字符串长度)
int len; //buf数组中未使用的字节数量
int free; //字节数组,用于存储字符串
char buf[];
};

  如上图所示,SDS遵循C字符串以空字符结尾的惯例,但是保存空字符的一字节空间不计算在SDS的len属性中。即对于SDS的结构满足:buf的长度 = len + free + 1。即当SDS的len=5,free=0字节时,则buf的长度为 5+0+1=6字节。

  C字符串本身的两个问题有:1.获取字符串长度的复杂度高  2.由于C字符串不记录自身长度容易造成缓冲区溢出等问题。C字符串修改字符串时会有大量的内存重分配操作,如拼接字符串时,如果不进行内存重分配,可能会造成缓冲区溢出;进行缩短字符串操作时,不进行内存重分配释放不再使用的那部分空间,则会产生内存泄露。

  为了解决上述两个问题,SDS做了一系列的改进操作。

  (1)由于SDS将字符串的长度存储在len属性中,所以SDS获取字符串长度的时间复杂度为O(1)。

  (2)SDS通过设计两种空间分配策略来减少字符串修改时带来的内存重分配次数,同时杜绝了缓冲区溢出的可能性

SDS的两种空间分配优化策略:

  SDS的优化策略是通过未使用空间(即free标记的空间)实现的

  (1)空间预分配:用于优化SDS字符串增长操作。当SDS的API对SDS进行修改并且需要进行空间扩展时,程序不仅会为SDS分配修改所必要的空间,还会为SDS分配额外的未使用空间。其中主要分为两点:当len<1MB时,程序分配和len同样大小的未使用空间,即free=len;当len>=1MB时,free=1MB。

  (2)惰性空间释放:用于优化SDS的字符串缩短操作。当SDS的API需要缩短SDS保存的字符串时,程序不会马上使用内存重分配来回收缩短后多出来的空间,而是使用 free 属性将这些字节的数量记录起来,以供将来使用。(缩短重分配操作,并未将来可能有的增长操作进行了优化)。

  

三. 链表

  Redis中链表可以用来实现列表键、发布与订阅、慢查询、监视器等功能。

  链表由两种结构组成,分别是list结构和listNode结构,它们的表示如下所示:

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;
typedef struct listNode{
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value;//节点值
}listNode;

  根据代码可以知道,list结构拥有一个指向链表表头和一个指向链表表尾的指针,而listNode中有一个前置指针和后置指针,因此,链表获得头、尾节点的时间复杂度为O(1),且可以从任意一端开始遍历。此外,list中还存有len属性保存链表长度,因此获得链表长度的时间复杂度仅为O(1)。

四. 字典

  Redis中字典可用于实现数据库和哈希键等。

  字典使用哈希表作为底层实现,哈希表dictht和哈希表节点dictEntry结构如下所示:

typedef struct dictht{
disctEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码,为size-1,用于计算索引值
unsigned long used;//已有节点数
} dictht;
typedef struct dictEntry{
void *key;//键
union{//值
void *val;
unint64_t u64;
int64_t s64;
} v;
struct dicEntry *next;//指向下个哈希表节点
} dictEntry;

  由结构代码和图可知,dictht结构中size属性为哈希表的总大小,used为哈希表节点个数;dictEntry节点中存储了键值对和指向下一个节点的指针。而dictht中sizemask属性总等于size-1,该属性值用于哈希算法。

  字典结构则如下所示:

typedef struct dict{
dictType *type;//类型特定函数
void *privdata;//私有数据
dictht ht[];//两个哈希表
int rehashidx;//用于标记是否处于rehash状态
} dict;

  字典由dict结构表示,其属性type是指向dictType结构的指针,该结构中保存了一簇用于操作特定类型键值对的函数;privdata属性则保存了需要传给这些函数的可选参数;rehashIdx则用于标记当前字典是否处于rehash(重新哈希)状态,rehashidx=-1时未进行rehash。(图示中略有错误,解决冲突时,链地址法是将新节点插入头部,即头插法,所以应当k2在前,k1在后)

  字典的哈希算法:每当一个新键值对添加到字典中时,程序需要先根据键计算出哈希值和索引值,再根据索引值将包含键值对的哈希节点放到哈希表数组的指定位置。哈希值使用字典的type中存储的哈希函数(hashFunction)计算(当字典被用作数据库或哈希键(HASH-key)的底层实现时,Redis使用MurmurHash2算法),而索引值则根据哈希表的sizemask和哈希值计算,index = 哈希值 & sizemask。例,新增键的哈希值为8,则上图新增键在ht[0]索引值为 8 & 3 = 0。

  处理键冲突:Redis的哈希表采用链地址法解决键冲突的问题,且为了速度考虑,每次都是将新节点添加到链表的表头位置(复杂度为O(1))。

  哈希表的扩展与收缩:负载因子 load_factor = ht[0].used / ht[0].size

    (1)当服务器未执行BGSAVE命令或者BGREWRITEAOF命令时,且哈希表的负载因子大于等于1时,自动扩展。

    (2)当服务器正在执行BGSAVE命令或者BGREWRITEAOF命令时,且哈希表的负载因子大于等于5时,自动扩展。

    (3)当哈希表的负载因子小于0.1时,程序对哈希表自动收缩。

    之所以有(1)、(2)的区别,是因为在执行这些命令的过程中,Reis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制技术来优化紫禁城的使用效率;因此,子进程存在期间,服务器会提高进行扩展操作所需的负载因子,尽可能避免子进程存在期间进行哈希表扩张操作,避免不必要的内存写入,最大限度的节约内存。

  渐进式rehash:当程序需要对哈希表的大小进行扩展或者收缩时,需要通过rehash操作来完成。

    (1)字典会为ht[1]的哈希表分配空间(扩展操作,ht[1]大小为第一个大于等于ht[0].used*2的2n;收缩操作,则ht[1]大小为第一个大于等于ht[0].used的2n)。

    (2)将保存在ht[0]上的键值对rehash到ht[1]上(即重新计算键的哈希值和索引值)。

    (3)当ht[0]上的键值对全部迁移完毕后,释放ht[0],并将ht[1]设置ht[0],再创建一个空白哈希表作为ht[1],为下次rehash准备。

    值得注意的是,rehash操作并不是一次性集中完成的,而是分多次、渐进式的完成。为了避免rehas对服务器性能造成影响,rehash采取了分而治之的方式,将rehash键值对所需的计算工作平摊到对字典的添加、删除、查找和更新操作上,从而避免集中式rehash带来了庞大计算量。

  

五. 跳跃表

  跳跃表是有序集合键的底层实现之一,它的结构由zskiplist和zskiplistNode组成,其结构和代码如下图所示

typedef struct zskiplistNode{
struct zskiplistNode *backward;//后退指针
double score;//分值
robj *obj;//成员对象
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度
}
} zskiplistNode;

  zskiplist保存跳跃表信息,header指向表头节点,tail指向表尾节点,level为跳跃表中的最大层数(表头节点层数不算在内),length为跳跃表长度(不包含表头节点)。

  zskiplistNode为跳跃表节点,level数组中可以包含多个元素分为多个层(每个跳跃表层高都是1~32之间的整数),每个层都有一个forward前进指针(用于表头向表尾方向访问)和一个span跨度(用于记录两个节点之间的距离以及记录排位的,所有指向NULL的前进指针跨度都为0);backward指针用于从表尾向表头方向遍历时使用(每次只能后退一个节点);score分值是一个double类型的浮点数,跳跃表中节点都按分值从小到大排序;obj属性是一个指向字符串对象的指针,而字符串对象保存着一个SDS值。

  同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。

  跳跃表中的节点按照分值大小顺序排列,当分值相同时,按照成员对象的大小排列。

六. 整数集合

  整数集合时Redis中用于保存整数值的集合抽象数据结构,其结构代码和图示如下所示:

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

  其中,encoding为intset的编码方式,length存储元素的数量,contents数组是整数集合的底层实现,其内的元素按从小到大的方式保存。contents数组的真正类型取决于encoding的值。

  整数集合的升级操作:每当一个类型比整数集合现有所有元素的类型都要长的新元素添加到整数集合中时,整数集合都需要先进行升级操作。

    (1)根据新元素的类型,扩展整数集合底层数组的空间大小,并未新元素分配空间。

    (2)将原来元素转换为新元素相同的类型,并从后往前依次放置原来的元素(放置过程中需位置底层数组的有序性质不变)

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

    从上可知,向整数集合添加新元素的时间复杂度为O(n)。

  升级的好处

    (1)通过自动升级来使用不同类型元素的数组,提升了整数集合的灵活性

    (2)尽可能节省内存。(如组织有在将int32_t类型存入时,原来的int16_t类型数组才会转换,不需要预先设定好)

七. 压缩列表

  压缩列表式列表建和哈希键的底层实现之一,是Redis为了节约内存而开发的,是一系列特殊编码的连续内存块组成的顺序型数据结构。其结构如下所示:

  

  zlbytes表示压缩列表总长度,zltail表示偏移量(用于记录气质地址到表尾节点的距离有多少字节),zllen为压缩列表节点个数,entry等都是压缩列表的节点,zlend用于标记压缩链表末端。而压缩列表节点中,previous_entry_length表示前一个节点长度(该属性长度可以是1字节或者5字节),encoding表示content属性保存的数据类型与长度,content负责保存节点值。

  如果前一个节点长度小于254字节,previous_entry_length长度为1字节;如果前一个节点长度大于等于254字节,previous_entry_length长度为5字节,后面4个字节保存前一个节点长度,第一个字节的值被设置为0x05。

  压缩列表从表尾向表头的遍历就是基于 previous_entry_length属性实现的(先要获得起始地址,再根据zltail获得指向表尾节点的指针,然后previous_entry_length属性计算出前一个节点的地址,便可依次从后往前遍历)。

  由于previous_entry_length属性记录前一个节点的长度,且该属性的长度由前一个节点的长度决定,因此在某些特殊情况下,删除或者增加节点可能会造成连锁更新(即特殊情况下产生的连续多次空间扩展操作)。例如,原来压缩列表节点长度都小于254(确切的说是250~253之间),此时将一个长度大于254的节点放到他们之前,便会引起后一个节点previous_entry_length的长度变化,从而使后一个节点长度大于等于254,依次类推,就想多米诺骨牌一样造成连锁反应。删除节点时的特殊情况则刚好相反。

  连锁更新在最坏情况下复杂度为O(N2),但真正造成这种情况出现的操作并不多见。

Redis学习笔记(一):基础数据结构的更多相关文章

  1. Redis学习笔记一:数据结构与对象

    1. String(SDS) Redis使用自定义的一种字符串结构SDS来作为字符串的表示. 127.0.0.1:6379> set name liushijie OK 在如上操作中,name( ...

  2. Redis学习笔记之底层数据结构

    1.简单动态字符串(simple dynamic string, SDS) 定义: struct sdshdr {        int len;//记录buf中使用的字节数量        int ...

  3. redis学习笔记——内存映射数据结构

    内存映射数据结构 解决问题:当一个对象包含的元素数量并不多,或者元素本身的体积并不大时,使用代价高昂的内部数据结构并不是最好的办法. 内存映射数据结构是一系列经过特殊编码的字节序列,创建它们所消耗的内 ...

  4. Redis学习笔记之基础篇

    Redis是一款开源的日志型key-value数据库,目前主要用作缓存服务器使用. Redis官方并没有提供windows版本的服务器,不过微软官方开发了基于Windows的Redis服务器Micro ...

  5. Redis学习笔记~目录

    回到占占推荐博客索引 百度百科 redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合). ...

  6. Redis学习笔记4-Redis配置详解

    在Redis中直接启动redis-server服务时, 采用的是默认的配置文件.采用redis-server   xxx.conf 这样的方式可以按照指定的配置文件来运行Redis服务.按照本Redi ...

  7. Redis学习笔记(三)Redis支持的5种数据类型的总结

    继续Redis学习笔记(二)来说说剩余的三种数据类型. 三.列表类型(List) 1.介绍 列表类型可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的一段片段.列表类型内部是 ...

  8. Redis学习笔记(二)Redis支持的5种数据类型的总结之String和Hash

    引言 在Redis学习笔记(一)中我们已经会安装并且简单使用Redis了,接下来我们一起来学习下Redis支持的5大数据类型. 简介 Redis是REmote DIctionary Server(远程 ...

  9. Redis学习笔记(3)——Redis的命令大全

    Redis是一种nosql数据库,常被称作数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted se ...

  10. Redis学习笔记(1)——Redis简介

    一.Redis是什么? Remote Dictionary Server(Redis) 是一个开源的使用ANSI C语言编写.遵守BSD协议.支持网络.可基于内存亦可持久化的日志型.Key-Value ...

随机推荐

  1. Mysql Java 驱动安装

    怎么安装MYSQL的JDBC驱动 1.下载mysql for jdbc driver. http://dev.mysql.com/downloads/connector/j/5.0.html 2.解压 ...

  2. UOJ136 开学前的作文

    描述 红包是一个萌萌的男孩子. 红包由于 NOI 惨挂,直到前不久依然无心写作业.如今快开学了,他决定好好完成作业. 对于可以交电子稿的作文,红包有特殊的完成技巧,大致流程是依次选中一段内容→按下 C ...

  3. python第一篇:Python 字符串编

    Python字符串编码 字符串编码的前世今生 1. 一个字节由8个bit组成,所以1个字节能表示的最大数为255: 2. 计算机是美国人发明的,所以一个字节可以表示所有的字符了,所以ASCII就成为美 ...

  4. css 中 div垂直居中的方法

    在说到这个问题的时候,也许有人会问CSS中不是有vertical-align属性来设置垂直居中的吗?即使是某些浏览器不支持我只需做少许的CSS Hack技术就可以啊!所以在这里我还要啰嗦两句,CSS中 ...

  5. python3与Redis连接操作

    Python3之redis使用   简介 redis是一个key-value存储系统,和Memcache类似,它支持存储的value类型相对更多,包括string(字符串),list(链表),set( ...

  6. Web中常用字体介绍

    1.在Web编码中,CSS默认应用的Web字体是有限的,虽然在新版本的CSS3,我们可以通过新增的@font-face属性来引入特殊的浏览器加载字体. 浏览器中展示网页文字内容时,文字字体都会按照设计 ...

  7. leetcode 231 Power of Two(位运算)

    Given an integer, write a function to determine if it is a power of two. 题解:一次一次除2来做的话,效率低.所以使用位运算的方 ...

  8. Sqlite表结构读取工具,word批量转html,在线云剪贴板,文件批量提取工具;

    工欲善其事必先利其器,本周为您推荐工具排行 Sqlite表结构读取工具,word批量转html,在线云剪贴板,文件批量提取工具:     本周我们又要发干货了,准备好接受了吗? 为什么是干货,就是因为 ...

  9. 非系统表空间损坏,rman备份恢复

    实验条件:有完整可用备份--查询表空间情况SQL> select tablespace_name,status from dba_tablespaces;TABLESPACE_NAME STAT ...

  10. 洛谷【P3379】【模板】最近公共祖先(LCA)

    浅谈\(RMQ\):https://www.cnblogs.com/AKMer/p/10128219.html 题目传送门:https://www.luogu.org/problemnew/show/ ...