c++ map底层_深入浅出Redisredis底层数据结构(上)

47b449741ad548910c8fa4574287e869.png

来源:http://t.cn/AigEOwRE

相信使用过Redis 的各位同学都很清楚,Redis 是一个基于键值对(key-value)的分布式存储系统,与Memcached类似,却优于Memcached的一个高性能的key-value数据库。

    在《Redis设计与实现》这样描述:

    Redis 数据库里面的每个键值对(key-value) 都是由对象(object)组成的:

      数据库键总是一个字符串对象(string object);

      数据库的值则可以是字符串对象、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合(sort set)对象这五种对象中的其中一种。

    我们为什么会说Redis 优于Memcached 呢,因为Redis 的出现,丰富了memcached 中key-value的存储不足,在部分场合可以对关系数据库起到很好的补充作用,而且这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。

    我们今天探讨的并不是Redis 中value 的数据类型,而是他们的具体实现——底层数据类型

    Redis 底层数据结构有一下数据类型:

    1.  简单动态字符串

    2.    链表

    3.    字典

    4.    跳跃表

    5.    整数集合

    6.    压缩列表

    7.    对象

    我们接下来会一步一步的探讨这些数据结构有什么特点,已经他们是如何构成我们所使用的value 数据类型。

2、简单动态字符串(simple dynamic string)SDS


2.1 概述

   Redis 是一个开源的使用ANSI C语言编写的key-value 数据库,我们可能会较为主观的认为 Redis 中的字符串就是采用了C语言中的传统字符串表示,但其实不然,Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作Redis 的默认字符串表示:

redis>SET msg "hello world"
OK

   设置一个key= msg,value = hello world 的新键值对,他们底层是数据结构将会是:

     键(key)是一个字符串对象,对象的底层实现是一个保存着字符串“msg” 的SDS;

     值(value)也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world” 的SDS

   从上述例子,我们可以很直观的看到我们在平常使用redis 的时候,创建的字符串到底是一个什么样子的数据类型。除了用来保存字符串以外,SDS还被用作缓冲区(buffer)AOF模块中的AOF缓冲区。

2.2  SDS 的定义

  Redis 中定义动态字符串的结构:

/*
 * 保存字符串对象的结构
 */  
struct sdshdr {
      
    // buf 中已占用空间的长度
    int len;
  
    // buf 中剩余可用空间的长度
    int free;
  
    // 数据空间
    char buf[];
};

   3c5ecf409f441f74675b27126809d2c9.png

   1、len 变量,用于记录buf 中已经使用的空间长度(这里指出Redis 的长度为5)

   2、free 变量,用于记录buf 中还空余的空间(初次分配空间,一般没有空余,在对字符串修改的时候,会有剩余空间出现)

     3、buf 字符数组,用于记录我们的字符串(记录Redis)

2.3  SDS 与 C 字符串的区别

    传统的C 字符串 使用长度为N+1 的字符串数组来表示长度为N 的字符串,这样做在获取字符串长度,字符串扩展等操作的时候效率低下。C 语言使用这种简单的字符串表示方式,并不能满足Redis 对字符串在安全性、效率以及功能方面的要求

2.3.1 获取字符串长度(SDS O(1)/C 字符串 O(n))

     传统的C 字符串 使用长度为N+1 的字符串数组来表示长度为N 的字符串,所以为了获取一个长度为C字符串的长度,必须遍历整个字符串。

     和C 字符串不同,SDS 的数据结构中,有专门用于保存字符串长度的变量,我们可以通过获取len 属性的值,直接知道字符串长度。

    7cd7c203dcc247e5859bf0f2c1f99cf8.png

2.3.2 杜绝缓冲区溢出

    C 字符串 不记录字符串长度,除了获取的时候复杂度高以外,还容易导致缓冲区溢出。

     假设程序中有两个在内存中紧邻着的 字符串 s1 和 s2,其中s1 保存了字符串“redis”,二s2 则保存了字符串“MongoDb”:

      5c199414dd349cb0ee7705981ad50821.png

     如果我们现在将s1 的内容修改为redis cluster,但是又忘了重新为s1 分配足够的空间,这时候就会出现以下问题:

      592200c131db143868b4cc031a0f9397.png

      我们可以看到,原本s2 中的内容已经被S1的内容给占领了,s2 现在为 cluster,而不是“Mongodb”。

     Redis 中SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:

     当我们需要对一个SDS 进行修改的时候,redis 会在执行拼接操作之前,预先检查给定SDS 空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作

1e111e730329e41a59b422ff7b76b886.png969ac21a5c478b8b213e251b3236d8ed.png

2.3.3 减少修改字符串时带来的内存重分配次数   

  C语言字符串在进行字符串的扩充和收缩的时候,都会面临着内存空间的重新分配问题。

   1. 字符串拼接会产生字符串的内存空间的扩充,在拼接的过程中,原来的字符串的大小很可能小于拼接后的字符串的大小,那么这样的话,就会导致一旦忘记申请分配空间,就会导致内存的溢出。

   2. 字符串在进行收缩的时候,内存空间会相应的收缩,而如果在进行字符串的切割的时候,没有对内存的空间进行一个重新分配,那么这部分多出来的空间就成为了内存泄露。

  举个例子:我们需要对下面的SDS进行拓展,则需要进行空间的拓展,这时候redis 会将SDS的长度修改为13字节,并且将未使用空间同样修改为1字节 

  91499db35ca345de0f84235b517c4189.pngd61533f2d63c7e876c04dc02fa9b5580.png

   因为在上一次修改字符串的时候已经拓展了空间,再次进行修改字符串的时候会发现空间足够使用,因此无须进行空间拓展

  0ecabe5fb25ae5c7b8eaf442e68d373e.png

  通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次

2.3.4 惰性空间释放

    我们在观察SDS 的结构的时候可以看到里面的free 属性,是用于记录空余空间的。我们除了在拓展字符串的时候会使用到free 来进行记录空余空间以外,在对字符串进行收缩的时候,我们也可以使用free 属性来进行记录剩余空间,这样做的好处就是避免下次对字符串进行再次修改的时候,需要对字符串的空间进行拓展。

    然而,我们并不是说不能释放SDS 中空余的空间,SDS 提供了相应的API,让我们可以在有需要的时候,自行释放SDS 的空余空间。

    通过惰性空间释放,SDS 避免了缩短字符串时所需的内存重分配操作,并未将来可能有的增长操作提供了优化

2.3.5 二进制安全

    C 字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的二进制数据。

    但是在Redis中,不是靠空字符来判断字符串的结束的,而是通过len这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的。

    例如:

   05d3ade33f8fad281b1ad2e1353cdcfe.png

2.3.6 兼容部分C字符串函数

     虽然SDS 的API 都是二进制安全的,但他们一样遵循C字符串以空字符串结尾的惯例。

2.3.7 总结

C 字符串SDS
获取字符串长度的复杂度为O(N)获取字符串长度的复杂度为O(1)
API 是不安全的,可能会造成缓冲区溢出API 是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次最多执行N次内存重分配
只能保存文本数据可以保存二进制数据和文本文数据
可以使用所有库中的函数可以使用一部分库中的函数

3、链表


3.1 概述

  链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。

  链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。

3.2 链表的数据结构

   每个链表节点使用一个 listNode结构表示(adlist.h/listNode):

typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;
}

   多个链表节点组成的双端链表:

de4116e29ddde5b2890a02c3d3f6702b.png

     我们可以通过直接操作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 组成的结构图:

de705a752a8994f623925aed995296e9.png

3.3 链表的特性

  • 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)

  • 无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对立案表的访问时以NULL为截止

  • 表头和表尾:因为链表带有head指针和tail 指针,程序获取链表头结点和尾节点的时间复杂度为O(1)

  • 长度计数器:链表中存有记录链表长度的属性 len

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

4、字典


4.1 概述

    字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。 

    在字典中,一个键(key)可以和一个值(value)进行关联,字典中的每个键都是独一无二的。在C语言中,并没有这种数据结构,但是Redis 中构建了自己的字典实现

    举个简单的例子:

redis > SET msg "hello world"
OK

    创建这样的键值对(“msg”,“hello world”)在数据库中就是以字典的形式存储

4.2 字典的定义

   4.2.1 哈希表

   Redis 字典所使用的哈希表由 dict.h/dictht 结构定义:typedef

struct dictht {

   //哈希表数组
   dictEntry **table;
   //哈希表大小
   unsigned long size;

   //哈希表大小掩码,用于计算索引值
   unsigned long sizemask;
   //该哈希表已有节点的数量
   unsigned long used;
}

   一个空的字典的结构图如下:

5da6db82eaeeadef08b642375f251052.png

   我们可以看到,在结构中存有指向dictEntry 数组的指针,而我们用来存储数据的空间既是dictEntry

         4.2.2 哈希表节点( dictEntry )

   dictEntry 结构定义:

typeof struct dictEntry{
   //键
   void *key;
   //值
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;

}

   在数据结构中,我们清楚key 是唯一的,但是我们存入里面的key 并不是直接的字符串,而是一个hash 值,通过hash 算法,将字符串转换成对应的hash 值,然后在dictEntry 中找到对应的位置。

        这时候我们会发现一个问题,如果出现hash 值相同的情况怎么办?Redis 采用了链地址法:

   60d70eabe6d331ff81aa1839e07adfdf.png

   当k1 和k0 的hash 值相同时,将k1中的next 指向k0 想成一个链表。

   4.2.3 字典

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privedata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    in trehashidx;

}

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

    ht 属性是一个包含两个项(两个哈希表)的数组

    普通状态下的字典:

34eb46f2cf05ddeefa8e5b0ce139d77f.png

4.3 解决哈希冲突

   在上述分析哈希节点的时候我们有讲到:在插入一条新的数据时,会进行哈希值的计算,如果出现了hash值相同的情况,Redis 中采用了连地址法(separate chaining)来解决键冲突。每个哈希表节点都有一个next 指针,多个哈希表节点可以使用next 构成一个单向链表,被分配到同一个索引上的多个节点可以使用这个单向链表连接起来解决hash值冲突的问题。

  举个例子:

  现在哈希表中有以下的数据:k0 和k1

eff50f7180e11af115ed80806a661c1b.png

    我们现在要插入k2,通过hash 算法计算到k2 的hash 值为2,即我们需要将k2 插入到dictEntry[2]中:

    427cf0c791d8fb60d0b9297fab103d22.png

     在插入后我们可以看到,dictEntry指向了k2,k2的next 指向了k1,从而完成了一次插入操作(这里选择表头插入是因为哈希表节点中没有记录链表尾节点位置)

4.4 Rehash

  随着对哈希表的不断操作,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成。

  4.4.1 目前的哈希表状态:

    我们可以看到,哈希表中的每个节点都已经使用到了,这时候我们需要对哈希表进行拓展。

db914258c6f5909590592c0844098288.png

  4.4.2 为哈希表分配空间

    哈希表空间分配规则:

      如果执行的是拓展操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂

      如果执行的是收缩操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂

    因此这里我们为ht[1] 分配 空间为8,

  58de49056b657dfc144564fa63f9bc58.png

  4.4.3 数据转移

    将ht[0]中的数据转移到ht[1]中,在转移的过程中,需要对哈希表节点的数据重新进行哈希值计算

    数据转移后的结果:

  ca77ad01609ef18b2159bceca0f968fd.png

   4.4.4 释放ht[0]

    将ht[0]释放,然后将ht[1]设置成ht[0],最后为ht[1]分配一个空白哈希表:

  3d7e5ca88221ad087f80e7cfce33e403.png

  4.4.5 渐进式 rehash

    上面我们说到,在进行拓展或者压缩的时候,可以直接将所有的键值对rehash 到ht[1]中,这是因为数据量比较小。在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。

    渐进式rehash 的详细步骤:

      1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

      2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始

      3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一

      4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束

    采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。

更多精彩,关注我吧

4bf61055eed7aba9ecc1799c01dd613e.png

近期推荐:

漫画:原创|木兰从军之外观模式

漫画:星球入侵之策略模式

分页场景(limit,offset)为什么会慢

你编写的Java代码是咋跑起来的?

文章好看点这里

f62dc4ebc70aa760f7993c329b7c92b4.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/502350.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

绝对布局优势_前瞻布局+尖端科技+雄厚资金 恒大解锁造车的“正确姿势”

新能源汽车行业风起云涌,在造车新势力们交相厮杀之时,恒大闪电般完成“新能源车全产业链布局”,占据了绝对先发优势。先人一步驶入快车道的恒大并未止步于此,5月30日,恒大又宣布拿下英国顶级轮毂电机企业protean&#…

山东省计算机考试无法报名,山东省2017年9月全国计算机等级考试报名事项公告...

根据教育部考试中心《关于做好2017年全国计算机等级考试工作的通知》(教试中心函〔2016〕237号)和《关于做好2017年9月全国计算机等级考试报名工作的通知》(鲁招考[2017]67号)要求,现将2017年9月我省全国计算机等级考试报名有关事项公告如下:一、 考试科…

laravel redis_thinkphp+redis实现秒杀,缓存等功能

秒杀是商城常见功能 phpredis是最常见的秒杀功能1,安装redis,根据自己的php版本安装对应的redis扩展首先查看phpinfo();php环境信息2,下载redishttps://windows.php.net/downloads/pecl/snaps/redis/ https://windows.php.net/downloads/pecl…

freemarker ftl模板_效率提升百分之四十,AS模板也太好用了吧

点击蓝字 轻松关注CV 工程师你好,以下内容会让你感觉到不适,慎入!在平时的开发工作中,我们经常将一个地方的代码,复制粘贴到另外一个地方,俗称搬砖,搬砖搬多了,作为一个资深的挨踢民…

python for循环连续输入五个成绩判断等级_Python基础(1)——输入输出/循环/条件判断/基本数据类型...

一、编程语言 1、 编译型语言:先编译,再执行 (先编译成二进制) 举例:英文书翻译成中文再看 C、C、C# 2、解释型语言:一边执行一边编译 举例:英文书找个翻译,它翻译一行你听一行 php、…

笔记本计算机无法开机怎么办,笔记本开机没反应,教您笔记本电脑开不了机怎么处理...

笔记本电脑使用的时间长了,都是会出现一些小故障,大毛病什么的,比如说像开机后,电源指示灯亮,但是电脑的显示器屏就是没有反应。这个问题让用户慌了神,为此,那笔记本电脑开不了机怎么解决&#…

离群点检测方法_干货 :时间序列异常检测

异常检测(Anomaly detection)是目前时序数据分析最成熟的应用之一,定义是从正常的时间序列中识别不正常的事件或行为的过程。有效的异常检测被广泛用于现实世界的很多领域,例如量化交易,网络安全检测、自动驾驶汽车和大型工业设备的日常维护。以在轨航天器为例,由于航天器昂…

centos6安装mysql并远程连接_MySQL5.7数据库安装与远程连接

系统:Redhat Linux 7.7MySQL版本:5.71. MySQL下载o 登录网站:https://dev.mysql.com/downloads/mysql/o 选择合适版本与系统Select version:5.7.31Select operating system:Red Hat Enterprise LinuxSelect OS Version…

python小代码_Python爬虫入门有意思的小长代码

一段有意思的代码,有兴趣的可以研究研究。 需求 用户收到短信如:购买了电影票或者火车票机票之类的事件。然后app读取短信,解析短信,获取时间地点,然后后台自动建立一个备忘录,在事件开始前1小时提醒用户。…

小学生 计算机编程 教程,面向小学生的C ++有趣编程(第一卷)配置教学资源课件完整版...

难度适中,易学易教的“面向小学生的C 趣味编程”教科书是小学信息学教学的重要组成部分.选择80多个接近小学生学习生活的例子,结合小学生的认知规律,激发孩子的兴趣,关注程序,适当削弱语法.使用流程图来阐明思想&#…

python怎么用for循环找出最大值_如何获取Python简单for循环索引

如何获取Python简单for循环索引 这篇文章主要介绍了如何获取Python简单for循环索引,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Python的for循环相比其他语言而言更加简单,比如经常会有如下这样类…

2018年江苏省计算机小高考,江苏2018年小高考成绩查询系统网站入口:江苏省教育考试院...

【导语】无忧考网从江苏省教育考试院获悉,江苏2018年小高考成绩查询系统网站入口将于4月9日上午正式开通!江苏省教育考试院关于发布江苏省2018年普通高中学业水平测试必修科目考试成绩的通告江苏省2018年普通高中学业水平测试必修科目考试成绩拟定于4月9…

列注释_【EXCEL检查问题】:如何快速检查并删除EXCEL中隐藏的工作表、行、列等信息...

前注:本案例是以EXCEL2016为示范软件,各版本的部分功能和路径可能不同在EXCEL使用过程中,你是否遇到过某一列的公式怎么修改都报错的情况?你是否遇到过一个只有区区几行数据的表格,却占用了好几百KB甚至好几M的空间呢&…

python输入的字符串转换为对应的数字类型_Python合集之Python运算符(四)

在上一节的合集中,我们了解了Python运算符中的位运算符的相关知识,本节我们将进一步了解一下Python运算符中运算符的优先级级基础的输入输出语法的相关知识。1. 运算符优先级Python运算符的运算规则是:优先级高的运算先执行,优先级…

服务器选购seo优化规则,需要做SEO的网站,购买服务器请注意六点

企业为了能在市场中争抢到一席之地都在对网站做各种优化,期望通过互联网,在最大程度上提高知名度,吸引客户。企业搭建网站一般都会考虑网站上线之后的优化问题,而服务器对于网站SEO的优化有很大的影响。网站想要做好SEO优化&#…

linux unix域socket_python3从零学习-5.8.1、socket—底层网络接口

源代码: Lib/socket.py这个模块提供了访问BSD*套接字*的接口。在所有现代Unix系统、Windows、macOS和其他一些平台上可用。这个Python接口是用Python的面向对象风格对Unix系统调用和套接字库接口的直译:函数 socket() 返回一个 套接字对象 ,其方法是对各…

freetype字体位图转距离场_那些火遍ins的字体小技巧,不看你就亏了

转载自版式设计很简单(ID:format_design)今天分享一个台湾设计师施博瀚的学习教程,看了以后,你会惊呼,原来这个是用AI做出来的?Ps.软件截图跟我们有所差别,以文字说明为主。www.instagram.com/bohanshih快速…

已知三角形三点坐标求角度_细心研磨椭圆焦点三角形,这肯定是最全的解释。...

点击上方蓝字关注我因为月考赶上运动会,继国庆之后,感觉又放了一个小长假。原本身体是很愿意的,可是,刚讲的解析几何突然被中断了,思想上还真是有点矛盾。因为,想了想两天后该讲些什么,脑中却一…

社会计算机比赛,哈尔滨工业大学社会计算与信息检索研究中心 – 理解语言,认知社会 » IR-Lab参加计算机学院“光熙杯”篮球赛...

IR-Lab参加计算机学院“光熙杯”篮球赛2014年04月18日迎首战师生一心,IR队旗开得胜 5月11日是“光熙杯”篮球赛的第二个比赛日,IR队迎来了小组赛的首场比赛,对阵空间计算队。比赛开始阶段,双方均略显紧张,然而随着比赛…

12v小型电机型号大全_电机型号参数大全

电动机型号是便于使用、设计、制造等部门进行业务联系和简化技术文件中产品名称、规格、型式等叙述而引用的一种代号。下面为大家介绍电动机型号含义等信息。一、电动机型号组成及含义由电机类型代号、电机特点代号、设计序号和励磁方式代号等四个小节顺序组成。1、类型代号是表…