前言

参考资料:《Redis设计与实现 第二版》;

本篇笔记按照书里的脉络,将知识点分为四个部分。其中第一部分数据结构与对象分为上中下篇,上篇包括:SDS链表字典;中篇包括跳跃表整数集合压缩列表;下篇为对象


1. 简单动态字符串

  • Redis构建一种名为简单动态字符串(SDS)的抽象类型,并将SDS作为Redis的默认字符串表示;
  • 除了用作字符串值外,SDS还被用作缓冲区buffer,如:AOF模块中的AOF缓冲区、客户端状态中的输入缓冲区;

1.1 SDS的定义

  • SDS的定义在sds.h/sdshdr结构:
    1. struct sdshdr {
    2. //记录buf数组中已使用字节数量
    3. //等于SDS所保存字符串长度
    4. int len;
    5. //记录buf数组中未使用字节数量
    6. int free;
    7. //字节数组,保存字符串
    8. char buf[];
    9. }

1.2 空间预分配与惰性空间释放

  • SDS相比C字符串的优点

    • 在常数时间复杂度内获取字符串长度;
    • 杜绝缓冲区溢出(空间预分配);
    • 减少修改字符串时带来的内存重分配次数(空间预分配);
    • 可保存二进制(使用len值判断字符串是否结束而不是 \0 );
    • 兼容部分C字符串函数(在字符串末尾保留空字符 \0
  • 通过未使用空间free,SDS实现空间预分配惰性空间释放
    • 空间预分配:用于SDS字符串增长。修改后小于1MB,则 len = free;反之分配1MB额外空间;
    • 惰性空间释放:用于SDS字符串缩短。即在有需要时才回收内存;

1.3 SDS的API

函数 作用 时间复杂度
sdsnew 创建一个包含给定C字符串的SDS O(N),N为给定C字符串的长度
sdsempty 创建一个不包含任何内容的空SDS O(1)
sdsfree 释放给定的SDS O(N),N为被释放SDS的长度
sdslen 返回SDS的已使用空间字节数 O(1),通过读取SDS的len属性获得
sdsavail 返回SDS的未使用空间字节数 O(1),通过读取free属性获得
sdsdup 创建一个给定SDS的副本(copy) O(N),N为给定SDS的长度
sdsclear 清空SDS保存的字符串内容 O(1),因为惰性空间释放策略
sdscat 将给定C字符串拼接到SDS字符串的末尾 O(N),N为被拼接字符串的长度
sdscatsds 将给定SDS字符串拼接到另一个SDS字符串的末尾 O(N),N为被拼接SDS字符串的长度
sdscpy 将给定的C字符串复制到SDS里面,覆盖原有字符串 O(N),N为被复制的C字符串长度
sdsgrowzero 用空字符串将SDS扩展至给定长度 O(N),N为扩展新增的字节数
sdsrange 保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除 O(N),N为扩展新增的字节数
sdstrim 接受一个SDS和一个C字符串作为参数,从SDS中移除所有在C字符串出现过的字符 O(N2),N为给定C字符串的长度
sdscmp 对比两个SDS字符串是否相同 O(N),N为两个SDS中较短的那个SDS的长度

2. 链表

  • C语言没有内置链表,所以Redis构建自己的链表;
  • 链表在Redis里的应用:发布与订阅、慢查询、监视器、Redis服务器保存多个客户端、列表键底层等、构建客户端输出缓冲区(output buffer);

2.1 链表与节点的定义

  • 链表节点的定义与实现在adlist.h/listNode结构里;

    1. typedef struct listNode {
    2. //前置节点
    3. struct listNode *prev;
    4. //后置节点
    5. struct listNode *next;
    6. //节点的值
    7. void *value;
    8. } listNode;
  • 链表的定义在adlist.h/list中:

    1. typedef struct list {
    2. //表头节点
    3. listNode *head;
    4. //表尾节点
    5. listNode *tail;
    6. //链表所包含的节点数量
    7. unsigned long len;
    8. //节点值复制函数
    9. void *(*dup)(void *ptr);
    10. //节点值释放函数
    11. void (*free)(void *ptr);
    12. //节点值对比函数
    13. int (*match)(void *ptr, void *key);
    14. } list;

2.2 链表的API

函数 作用 时间复杂度
listSetDupMethod 将给定的函数设置为链表的节点值复制函数 O(1),复制函数可以通过链表的dup属性直接获得
listGetDupMethod 返回链表当前正在使用的节点值复制函数 O(1)
listSetFreeMethod 将给定的函数设置为链表的节点值释放函数 O(1),释放函数可以通过链表的free属性直接获得
listGetFree 返回链表当前正在使用的节点值释放函数 O(1)
listSetMatchMethod 将给定的函数设置为链表的节点值对比函数 O(1),对比函数可以通过链表的match属性直接获得
listGetMatchMethod 返回链表当前正在使用的节点值对比函数 O(1)
listLength 返回链表的长度 O(1),链表长度可以通过链表的len属性直接获得
listFirst 返回链表的表头节点 O(1),表头节点可以通过链表的head属性直接获得
listLast 返回链表的表尾节点 O(1),表尾节点可以通过链表的tail属性直接获得
listPrevNode 返回给定节点的前置节点 O(1),前置节点可以通过节点的prev属性直接获得
listNextNode 返回给定节点的后置节点 O(1),前置节点可以通过节点的next属性直接获得
listNodeValue 返回给定节点的目前正在保存的值 O(1),节点值可以通过节点的value属性直接获得
listCreate 创建一个不包含任何节点的新链表 O(1)
listAddNodeHead 将一个包含给定值的新节点添加到给定链表的表头 O(1)
listAddNodeTail 将一个包含给定值的新节点添加到给定链表的表尾 O(1)
listInsertNode 将一个包含给定值的新节点添加到给定节点的之前或之后 O(1)
listSearchKey 查找并返回链表中包含给定值的节点 O(N),N为链表长度
listIndex 返回链表在给定索引上的节点 O(N),N为链表长度
listDelNode 从链表中删除给定节点 O(N),N为链表长度
listRotate 将链表的表尾节点弹出,然后将被弹出的节点插入到链表的表头,成为新的表头节点 O(1)
listDup 复制一个给定链表的副本 O(N),N为链表长度
listRelease 释放给定链表,以及链表中的所有节点 O(N),N为链表长度

3. 字典

  • 字典,又称符号表关联数组映射,用于保存键值对
  • Redis自己构建字典;
  • 字典在Redis里的应用:Redis数据库底层、哈希键的底层实现等;
  • Redis的字典使用哈希表作为底层实现;

3.1 哈希表与哈希节点

  • 字典所使用的哈希表的定义,在dict.h/dictht结构中:

    1. typedef struct dictht {
    2. //哈希表数组
    3. dictEntry **table;
    4. //哈希表大小
    5. unsigned long size;
    6. //哈希表大小掩码,用于计算索引值
    7. //总是等于size-1
    8. unsigned long sizemask;
    9. //该哈希表已有节点的数量
    10. unsigned long used;
    11. } dictht;
    • table是一个数组,数组的每个元素都是指向dict.h/dictEntry结构的指针;
  • 哈希表节点的定义,在dict.h/dictEntry结构;

    1. typedef struct dictEntry {
    2. //键
    3. void *key;
    4. //值
    5. union{
    6. void *val;
    7. uint64_t u64;
    8. int64_t s64;
    9. } v;
    10. //指向下个哈希表节点,形成链表
    11. struct dictEntry *next;
    12. } dictEntry;
    • next值的作用:将多个哈希值相同的键值对连接,解决键冲突问题(collision);

3.2 字典

  • 字典的定义,在dict.h/dict结构:

    1. typedef struct dict {
    2. //类型特定函数
    3. dictType *type;
    4. //私有数据
    5. void *privdata;
    6. //哈希表
    7. dictht ht[2];
    8. //rehash 索引
    9. //当 rehash 不在进行时,值为-1
    10. int trehashidx; /* rehashing not in progress if rehashidx == -1 */
    11. } dict;
    • type和privdata属性:针对不同类型键值对,为创建多态字典而设置;
    • type属性:是一个指向dictType的指针,Redis为用途不同的字典设置不同的dictType结构体,进而设置不同的类型特定函数;
    • privdata属性:保存了需要传给类型特定函数的可选参数;
    • ht[2]属性:每一项是dictht哈希表,一般字典只用ht[0]哈希表。对ht[0]进行rehash时使用ht[1];
    • trehashidx属性:记录当前rehash的进度;

3.3 哈希算法

  • Redis计算哈希值与索引值的方法:

    1. # 使用字典设置哈希函数,计算key的哈希值
    2. hash = dict -> type -> hashFunction(key)
    3. # 使用哈希表的sizemask属性和哈希值,计算索引值
    4. # 根据情况不同,ht[x]可以是ht[0]或者ht[1]
    5. index = hash & dict -> ht[x].sizemask
  • 当字典被用作数据库底层实现,或哈希键底层实现时,Redis使用MurmurHash2算法计算键的哈希值;

3.4 解决键冲突

  • 键冲突:有两个或以上的键被分配到哈希表数组的同一个索引;
  • Redis使用链地址法解决键冲突问题;
  • 链地址法dictEntry哈希节点里有个next属性,可以用其将索引值相同的节点连成链表;
    • 出于速度考虑,将新节点添加到链表表头,O(1);

3.5 rehash

  • 通过执行rehash(重新散列)来扩展和收缩哈希表
  • rehash的步骤:
    • 1)为ht[1]分配空间,若扩展,则ht[1].size为第一个大于等于ht[0].used*2的 2n。若收缩,则ht[1].size为第一个大于等于ht[0].used的 2n
    • 2)将ht[0]中的所有键值对rehash到ht[1]上;
    • 3)迁移完后,释放ht[0],将ht[1]设置为ht[0],创建一个空白哈希表ht[1]
  • 哈希表扩展与收缩的时机:
    • 负载因子的计算:load_factor = ht[0].used / ht[0].size
    • 扩展:服务器没有执行BGSAVEBGREWRITEAOF命令,并且负载因子大于等于1;
    • 扩展:服务器正在执行BGSAVEBGREWRITEAOF命令,并且负载因子大于等于5;
      • 避免在执行该命令(子进程存在期间)时进行扩展操作,避免不必要的内存写入操作;
    • 收缩:负载因子小于0.1;

3.6 渐进式rehash

  • 当键值对成万上亿时,需要分多次、渐进式完成rehash;
  • 渐进式rehash的步骤:
    • 1)为ht[1]分配空间;
    • 2)将字典的索引计数器变量rehashidx设置为0,表示rehash正式开始;
    • 3)rehash期间,每个对字典操作完成后,将rehashidx++
    • 4)当ht[0]中的所有键值对rehash到ht[1]后,rehashidx设置为 -1;
  • 渐进式hash期间:
    • 查找操作先查ht[0],再查ht[1]
    • 新增操作只在ht[1]新增,保证ht[0]只减不增;

3.7 字典的API

函数 作用 时间复杂度
dictCreate 创建一个新字典 O(1)
dictAdd 将给定的键值对添加到字典里 O(1)
dictReplace 将给定键值对添加到字典里,如果键已存在,则会用新值替换旧值 O(1)
dictFetchValue 返回给定键的值 O(1)
dictGetRandomKey 从字典中随机返回一个键值对 O(1)
dictDelete 从字典中删除给定键所对应的键值对 O(1)
dictRelease 释放字典,以及字典包含的键值对 O(N),N为字典包含的键值对数量

最后

新人制作,如有错误,欢迎指出,感激不尽!
欢迎关注公众号,会分享一些更日常的东西!
如需转载,请标注出处!

Redis | 第一部分:数据结构与对象 上篇《Redis设计与实现》的更多相关文章

  1. [redis读书笔记] 第一部分 数据结构与对象 对象类型

    - 从前面redis的基本数据结构来看,可以看出,redis都是在基本结构(string)的基础上,封装了一层统计的结构(SDS),这样让对基本结构的访问能够更快更准确,提高可控制度. - redis ...

  2. Redis笔记(1)数据结构与对象

    1.前言 此系列博客记录redis设计与实现一书的笔记,提取书本中的知识点,省略相关说明,方便查阅. 2.基本数据结构 2.1 简单动态字符串SDS(simple dynamic string) 结构 ...

  3. Redis 的底层数据结构(对象)

    目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层.最常用的数据结构,相信你也掌握的不错. 但 redis 实际存储键值对的时候,是基于对 ...

  4. 左手Mongodb右手Redis 第一章,进入Mongodb和Redis的世界

    ---恢复内容开始--- 1,为什么要使用非关系型数据库,关系型数据库咋滴,不能用嘛? 存在即合理,非关系型数据库的出现,那说明关系型数据库不适用了. 非关系型数据库(NOSQL)-->Not ...

  5. .net core工具组件系列之Redis—— 第一篇:Windows环境配置Redis(5.x以上版本)以及部署为Windows服务

    Cygwin工具编译Redis Redis6.x版本是未编译版本(官方很调皮,所以没办法,咱只好帮他们编译一下了),所以咱们先下载一个Cygwin,用它来对Redis进行编译. Cygwin下载地址: ...

  6. [REDIS 读书笔记]第一部分 数据结构与对象 跳跃表

    下面是跳跃表的基本原理,REDIS的实现大致相同 跳跃表的一个特点是,插入NODE是通过随机的方式来决定level的,比较奇特 下面是skipList的一个介绍,转载来的,源地址:http://ken ...

  7. [redis读书笔记] 第一部分 数据结构与对象 对象特性

    一 类型检查和多态    类型检查,即有的命令是只针对特定类型的,如果类型不对,就会报错,此处的类型,是指的键类型,即robj.type.下面为有类型检查的命令: 对于某一种类型,redis下底层的实 ...

  8. [redis读书笔记] 第一部分 数据结构与对象 字典

    三 字典 字典是Hash对象的底层实现,比如用HSET创建一个HASH的对象,底层可能就是用一个字典实现的键值对. 字典的实现主要设计下面三个结构: /* * 哈希表节点 */ typedef str ...

  9. [redis读书笔记] 第一部分 数据结构与对象 简单动态字符串

    本读书笔记主要来自于<<redis设计与实现>> -- 黄键宏(huangz) redis主要设计了字符串,链表,字典,跳跃表,整数集合,压缩列表来做为基本的数据结构,实现键值 ...

随机推荐

  1. NLP与深度学习(五)BERT预训练模型

    1. BERT简介 Transformer架构的出现,是NLP界的一个重要的里程碑.它激发了很多基于此架构的模型,其中一个非常重要的模型就是BERT. BERT的全称是Bidirectional En ...

  2. 数据库MySQL主从-GTID

    1.第一步在主服务器上/etc/my.cnf/下添加 log-bin=log-bin server-id=1 gtid_mode=ON enforce_gtid_consistency 2.第二步:重 ...

  3. SpringBoot如何实现定时任务

    写在前面 SpringBoot创建定时任务的方式很简单,主要有两种方式:一.基于注解的方式(@Scheduled)二.数据库动态配置.实际开发中,第一种需要在代码中写死表达式,如果修改起来,又得重启会 ...

  4. 给力!斩获 GitHub 14000 Star,两周创办开源公司获数百万美元融资

    文章来源|AI科技大本营 作者|伍杏玲 上世纪 90 年代初,21 岁大学生 Linus Torvalds 开源 Linux 操作系统,自此掀起全球开源浪潮.随后"中国 Linux 第一人& ...

  5. Python 面向对象笔记

    Python 面向对象课程笔记 前言 Python 面向对象 正文 基本概念 什么是对象: 万物皆对象 对象是具体物体: 拥有属性 拥有行为 封装零散为整体 OOP(Object Oriented P ...

  6. gin 源码阅读(5) - 灵活的返回值处理

    gin 源码阅读系列文章列表: gin 源码阅读(1) - gin 与 net/http 的关系 gin 源码阅读(2) - http请求是如何流入gin的? gin 源码阅读(3) - gin 路由 ...

  7. 第四次Scrum Metting

    日期:2021年4月29日 会议主要内容概述:交代近两日工作,进一步细化上次讨论细节,代码合并. 一.进度情况## 组员 负责 两日内已完成的工作 后两日计划完成的工作 工作中遇到的困难 徐宇龙 后端 ...

  8. RogrePirates Scrum Meeting 博客汇总

    RogrePirates 博客目录 一.Scrum Meeting 1.Alpha阶段 第一次会议 第二次会议 第三次会议 第四次会议 第五次会议 第六次会议 第七次会议 第八次会议 第九次会议 第十 ...

  9. Django+Vue跨域配置与经验

    一.原理 同源?同源策略? 同源的定义是:两个页面的协议.端口和域名都相同 同源的例子: 不同源的例子: 同源策略SOP(Same origin policy)是一种浏览器约定,它是浏览器最核心也最基 ...

  10. springboot整合rabbitmq实现生产者消息确认、死信交换器、未路由到队列的消息

    在上篇文章  springboot 整合 rabbitmq 中,我们实现了springboot 和rabbitmq的简单整合,这篇文章主要是对上篇文章功能的增强,主要完成如下功能. 需求: 生产者在启 ...