Redis 5设计与源码分析读书笔记

目录

  • 引言
    • Redis 5.0的新特性
    • Redis源码概述
    • Redis安装与调试
  • 简单动态字符串
    • 数据结构
    • 基本操作
      • 创建字符串
      • 释放字符串
      • 拼接字符串
        • 扩容策略
      • 其余API
    • 本章小结
      • 兼容C语言字符串、保证二进制安全
      • sdshdr5的特殊之处是什么
      • SDS是如何扩容的
  • 跳跃表
    • 简介
    • 跳跃表节点与结构
      • 跳跃表节点
      • 跳跃表结构
    • 基本操作
      • 创建跳跃表
        • 节点层高
        • 创建跳跃表节点
        • 头节点
        • 创建跳跃表的步骤
      • 插入节点
        • 调整跳跃表高度
        • 调整backward
      • 删除节点
        • 设置span和forward
      • 删除跳跃表
    • 跳跃表的应用
    • 小结
  • 压缩列表
    • 压缩列表的存储结构
    • 结构体
    • 连锁更新
  • 字典
    • 基本概念
      • 数组
      • Hash函数
      • Hash冲突
    • Redis字典的实现
      • Hash表
      • Hash表节点
      • 字典

引言

Redis是目前最流行的键值对(key-value)数据库,以出色的性能著称,官方提供的数据是可以支持100000以上的+QPS。Redis具有高性能的主要原因如下:

  • Redis是基于内存的存储数据库,绝大部分的命令处理只是纯粹的内存操作,内存的读写速度非常快。
  • Redis是单进程线程的服务(实际上一个正在运行的Redis Server肯定不止一个线程,但只有一个线程来处理网络请求)​,避免了不必要的上下文切换,同时不存在加锁/释放锁等同步操作。
  • Redis使用多路I/O复用模型(select、poll、epoll)​,可以高效处理大量并发连接。
  • Redis中的数据结构是专门设计的,增、删、改、查等操作相对简单。

Redis 5.0的新特性

  • 新增Streams数据类型,这是Redis 5.0最重要的改进之一。可以把Streams当作消息队列,详细内容参见后续章节。
  • 新的模块API、定时器、集群及字典。
  • RDB中持久化存储LFU和LRU的信息。
  • 将集群管理功能完全用C语言集成到redis-cli中,Redis 3.x和Redis 4.x的集群管理是通过Ruby脚本实现的。
  • 有序集合新增命令ZPOPMIN/ZPOPMAX。
  • 改进HyperLogLog的实现。
  • 新增Client Unblock和Client ID。
  • 新增LOLWUT命令。
  • Redis主从复制中的从不再称为Slave,改称Replicas。
  • Redis 5.0引入动态哈希,以平衡CPU的使用率和相应性能,可以通过配置文件进行配置。Redis 5.0默认使用动态哈希。
  • Redis核心代码进行了部分重构和优化。

Redis源码概述

Redis源代码主要存放在src文件夹中,作者没有整理这些文件,统一存放到了一个文件夹中,如图所示。其中server.c为服务端程序,redis-cli.c为客户端程序。
在这里插入图片描述
Redis源代码的核心部分主要如下。

  1. 基本的数据结构
    • 动态字符串sds.c
    • 整数集合intset.c
    • 压缩列表ziplist.c
    • 快速链表quicklist.c
    • 字典dict.c
    • Streams的底层实现结构listpack.c和rax.c
  2. Redis数据类型的底层实现
    • Redis对象object.c
    • 字符串t_string.c
    • 列表t_list.c
    • 字典t_hash.c
    • 集合及有序集合t_set.c和t_zset.c
    • 数据流t_stream.c
  3. Redis数据库的实现
    • 数据库的底层实现db.c
    • 持久化rdb.c和aof.c
  4. Redis服务端和客户端实现
    • 事件驱动ae.c和ae_epoll.c
    • 网络连接anet.c和networking.c
    • 服务端程序server.c
    • 客户端程序redis-cli.c
  5. 其他
    • 主从复制replication.c
    • 哨兵sentinel.c
    • 集群cluster.c
    • 其他数据结构,如hyperloglog.c、geo.c等
    • 其他功能,如pub/sub、Lua脚本

Redis安装与调试

通过网址http://download.redis.io/releases/可以获得各个版本的Redis源码,本书以Redis 5.0为例,下载源码包并编译安装(源码包URL为http://download.redis.io/releases/redis-5.0.0.tar.gz)​。

    $ wget http://download.redis.io/releases/redis-5.0.0.tar.gz$ tar -zxvf redis-5.0.0.tar.gz$ cd redis-5.0.0$ make$ cd src$make install

到此,我们完成了Redis 5.0的编译安装,生成的可执行文件在/usr/local/bin目录中:
redis-benchmark redis-check-aof redis-check-rdb redis-cli redis-sentinel redis-server
其中redis-benchmark是官方自带的Redis性能测试工具;当AOF文件或者RDB文件出现语法错误时,可以使用redis-check-aof或者redis-check-rdb修复;redis-cli是客户端命令行工具,可以通过命令redis-cli -h {host} -p {port}连接到指定Redis服务器;redis-sentinel是Redis哨兵启动程序;redis-server是Redis服务端启动程序。

简单动态字符串

简单动态字符串(Simple Dynamic Strings, SDS)是Redis的基本数据结构之一,用于存储字符串和整型数据。SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

数据结构

什么是二进制安全?通俗地讲,C语言中,用“\0”表示字符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

SDS既然是字符串,那么首先需要一个字符串指针;为了方便上层的接口调用,该结构还需要记录一些统计信息,如当前数据长度和剩余容量等,例如:

    struct sds {int len; // buf中已占用字节数int free; // buf中剩余可用字节数char buf[]; // 数据空间};

SDS结构示意如图所示,在64位系统下,字段len和字段free各占4个字节,紧接着存放字符串:在这里插入图片描述
Redis 3.2之前的SDS也是这样设计的。这样设计有以下几个优点:

  • 有单独的统计变量len和free(称为头部)​。可以很方便地得到字符串长度。
  • 内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。
  • 由于有长度统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全。

上例中的buf[​]是一个柔性数组。柔性数组成员(flexible array member)​,也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存

之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置)​;可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。

结合两个问题,5种类型(长度1字节、2字节、4字节、8字节、小于1字节)的SDS至少要用3位来存储类型(23=8),1个字节8位,剩余的5位存储长度,可以满足长度小于32的短字符串。在Redis 5.0中,我们用如下结构来存储长度小于32的短字符串:

    struct __attribute__ ((__packed__))sdshdr5 {unsigned char flags; /* 低3位存储类型,高5位存储长度 */char buf[]; /*柔性数组,存放实际内容*/};

sdshdr5结构中,flags占1个字节,其低3位(bit)表示type,高5位(bit)表示长度,能表示的长度区间为0~31(25-1), flags后面就是字符串的内容。
在这里插入图片描述
而长度大于31的字符串,1个字节依然存不下。我们按之前的思路,将len和free单独存放。sdshdr8、sdshdr16、sdshdr32和sdshdr64的结构相同,sdshdr16结构如图所示。
在这里插入图片描述
其中“表头”共占用了S[2(len)+2(alloc)+1(flags)]个字节。flags的内容与sdshdr5类似,依然采用3位存储类型,但剩余5位不存储长度。在Redis 5.0中,sdshdr8、sdshdr16、sdshdr32和sdshdr64的数据结构如下:

    struct __attribute__((__packed__))sdshdr8 {uint8_t len; /* 已使用长度,用1字节存储 */uint8_t alloc; /* 总长度,用1字节存储*/unsigned char flags; /* 低3位存储类型,高5位预留 */char buf[]; /*柔性数组,存放实际内容*/};struct __attribute__((__packed__))sdshdr16 {uint16_t len; /*已使用长度,用2字节存储*/uint16_t alloc; /* 总长度,用2字节存储*/unsigned char flags; /* 低3位存储类型,高5位预留 */char buf[]; /*柔性数组,存放实际内容*/};struct __attribute__((__packed__))sdshdr32 {uint32_t len; /*已使用长度,用4字节存储*/uint32_t alloc; /* 总长度,用4字节存储*/unsigned char flags; /* 低3位存储类型,高5位预留 */char buf[]; /*柔性数组,存放实际内容*/};struct __attribute__((__packed__))sdshdr64 {uint64_t len; /*已使用长度,用8字节存储*/uint64_t alloc; /* 总长度,用8字节存储*/unsigned char flags; /* 低3位存储类型,高5位预留 */char buf[]; /*柔性数组,存放实际内容*/};

可以看到,这4种结构的成员变量类似,唯一的区别是len和alloc的类型不同。结构体中4个字段的具体含义分别如下:

  1. len:表示buf中已占用字节数。
  2. alloc:表示buf中已分配字节数,不同于free,记录的是为buf分配的总长度。
  3. flags:标识当前结构体的类型,低3位用作标识位,高5位预留。
  4. buf:柔性数组,真正存储字符串的数据空间。

结构最后的buf依然是柔性数组,通过对数组指针作“减一”操作,能方便地定位到flags。

这样做有以下两个好:

  • 节省内存,例如sdshdr32可节省3个字节(12-9)​。
  • SDS返回给上层的,不是结构体首地址,而是指向内容的buf指针。因为此时按1字节对齐,故SDS创建成功后,无论是sdshdr8、sdshdr16还是sdshdr32,都能通过(char*)sh+hdrlen得到buf指针地址(其中hdrlen是结构体长度,通过sizeof计算得到)​。修饰后,无论是sdshdr8、sdshdr16还是sdshdr32,都能通过buf[-1]找到flags,因为此时按1字节对齐。若没有packed的修饰,还需要对不同结构进行处理,实现更复杂。

基本操作

数据结构的基本操作不外乎增、删、改、查,SDS也不例外。由于Redis 3.2后的SDS涉及多种类型,修改字符串内容带来的长度变化可能会影响SDS的类型而引发扩容。

创建字符串

创建SDS的大致流程:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。

  1. 创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8。
  2. 长度计算时有“+1”操作,是为了算上结束符“\0”​。
  3. 返回值是指向sds结构buf字段的指针。

从源码中我们可以看到,其实s就是一个字符数组的指针,即结构中的buf。这样设计的好处在于直接对上层提供了字符串内容指针,兼容了部分C函数,且通过偏移能迅速定位到SDS结构体的各处成员变量。

释放字符串

SDS提供了直接释放内存的方法——sdsfree,该方法通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存。

为了优化性能(减少申请内存的开销), SDS提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将SDS的len归零,此处已存在的buf并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。

所以真清除,假清除,要看安迪雷斯的心情了

拼接字符串

拼接字符串操作本身不复杂,可用sdscatsds来实现。sdscatsds是暴露给上层的方法,其最终调用的是sdscatlen。由于其中可能涉及SDS的扩容,sdscatlen中调用sdsMakeRoomFor对带拼接的字符串s容量做检查,若无须扩容则直接返回s;若需要扩容,则返回扩容好的新字符串s。函数中的len、curlen等长度值是不含结束符的,而拼接时用memcpy将两个字符串拼接在一起,指定了相关长度,故该过程保证了二进制安全。最后需要加上结束符。

扩容策略

扩容策略如下:

  • 若sds中剩余空闲长度avail大于新增内容的长度addlen,直接在柔性数组buf末尾追加即可,无须扩容
  • 若sds中剩余空闲长度avail小于或等于新增内容的长度addlen,则分情况讨论:新增后总长度len+addlen<1MB的,按新长度的2倍扩容;新增后总长度len+addlen>1MB的,按新长度加上1MB扩容。
  • 最后根据新长度重新选取存储类型,并分配空间。此处若无须更改类型,通过realloc扩大柔性数组即可;否则需要重新开辟内存,并将原字符串的buf内容移动到新位置。

其余API

SDS暴露给上层的是指向柔性数组buf的指针。读操作的复杂度多为O(1),直接读取成员变量;涉及修改的写操作,则可能会触发扩容。

本章小结

兼容C语言字符串、保证二进制安全

SDS对象中的buf是一个柔性数组,上层调用时,SDS直接返回了buf。由于buf是直接指向内容的指针,故兼容C语言函数。而当真正读取内容时,SDS会通过len来限制读取长度,而非“\0”​,保证了二进制安全。

sdshdr5的特殊之处是什么

sdshdr5只负责存储小于32字节的字符串。一般情况下,小字符串的存储更普遍,故Redis进一步压缩了sdshdr5的数据结构,将sdshdr5的类型和长度放入了同一个属性中,用flags的低3位存储类型,高5位存储长度。创建空字符串时,sdshdr5会被sdshdr8替代。

SDS是如何扩容的

SDS在涉及字符串修改处会调用sdsMakeroomFor函数进行检查,根据不同情况动态扩容,该操作对上层透明。

跳跃表

有序集合在生活中较常见,如根据成绩对学生进行排名、根据得分对游戏玩家进行排名等。对于有序集合的底层实现,我们可以使用数组、链表、平衡树等结构。数组不便于元素的插入和删除;链表的查询效率低,需要遍历所有元素;平衡树或者红黑树等结构虽然效率高但实现复杂。Redis采用了一种新型的数据结构——跳跃表。跳跃表的效率堪比红黑树,然而其实现却远比红黑树简单。

简介

通过将有序集合的部分节点分层,由最上层开始依次向后查找,如果本层的next节点大于要查找的值或next节点为NULL,则从本节点开始,降低一层继续向后查找,依次类推,如果找到则返回节点;否则返回NULL。采用该原理查找节点,在节点数量比较多时,可以跳过一些节点,查询效率大大提升,这就是跳跃表的基本思想。跳跃表的实现过程如图所示:
在这里插入图片描述
从图中我们可以看出跳跃表有如下性质:

  1. 跳跃表由很多层构成。
  2. 跳跃表有一个头(header)节点,头节点中有一个64层的结构,每层的结构包含指向本层的下个节点的指针,指向本层下个节点中间所跨越的节点个数为本层的跨度(span)​。
  3. 除头节点外,层数最多的节点的层高为跳跃表的高度(level)​,图中跳跃表的高度为3。
  4. 每层都是一个有序链表,数据递增。
  5. 除header节点外,一个元素在上层有序链表中出现,则它一定会在下层有序链表中出现。
  6. 跳跃表每层最后一个节点指向NULL,表示本层有序链表的结束。
  7. 跳跃表拥有一个tail指针,指向跳跃表最后一个节点。
  8. 最底层的有序链表包含所有节点,最底层的节点个数为跳跃表的长度(length)​(不包括头节点)​,图中跳跃表的长度为7。
  9. 每个节点包含一个后退指针,头节点和第一个节点指向NULL;其他节点指向最底层的前一个节点。

跳跃表每个节点维护了多个指向其他节点的指针,所以在跳跃表进行查找、插入、删除操作时可以跳过一些节点,快速找到操作需要的节点。归根结底,跳跃表是以牺牲空间的形式来达到快速查找的目的。跳跃表与平衡树相比,实现方式更简单,只要熟悉有序链表,就可以轻松地掌握跳跃表。

跳跃表节点与结构

跳跃表节点

下面我们来看跳跃表节点的zskiplistNode结构体:

    typedef struct zskiplistNode {sds ele;double score;struct zskiplistNode *backward;struct zskiplistLevel {struct zskiplistNode *forward;unsigned int span;} level[];} zskiplistNode;

该结构体包含如下属性:

  1. ele:用于存储字符串类型的数据。
  2. score:用于存储排序的分值。
  3. backward:后退指针,只能指向当前节点最底层的前一个节点,头节点和第一个节点——backward指向NULL,从后向前遍历跳跃表时使用。
  4. level:为柔性数组。每个节点的数组长度不一样,在生成跳跃表节点时,随机生成一个1~64的值,值越大出现的概率越低。level数组的每项包含以下两个元素:
    • orward:指向本层下一个节点,尾节点的forward指向NULL。
    • span:forward指向的节点与本节点之间的元素个数。span值越大,跳过的节点个数越多。

跳跃表是Redis有序集合的底层实现方式之一,所以每个节点的ele存储有序集合的成员member值,score存储成员score值。所有节点的分值是按从小到大的方式排序的,当有序集合的成员分值相同时,节点会按member的字典序进行排序。

跳跃表结构

除了跳跃表节点外,还需要一个跳跃表结构来管理节点,Redis使用zskiplist结构体,定义如下:

    typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length;int level;} zskiplist;

该结构体包含如下属性:

  1. header:指向跳跃表头节点。头节点是跳跃表的一个特殊节点,它的level数组元素个数为64。头节点在有序集合中不存储任何member和score值,ele值为NULL, score值为0;也不计入跳跃表的总长度。头节点在初始化时,64个元素的forward都指向NULL, span值都为0。(就是常用的哨兵节点)
  2. tail:指向跳跃表尾节点。
  3. length:跳跃表长度,表示除头节点之外的节点总数。
  4. level:跳跃表的高度。

基本操作

创建跳跃表

节点层高

节点层高的最小值为1,最大值是ZSKIPLIST_MAXLEVEL, Redis5中节点层高的值为64。Redis通过zslRandomLevel函数随机生成一个1~64的值,作为新建节点的高度,值越大出现的概率越低。节点层高确定之后便不会再修改。

创建跳跃表节点

跳跃表的每个节点都是有序集合的一个元素,在创建跳跃表节点时,待创建节点的层高、分值、member等都已确定。对于跳跃表的每个节点,我们需要申请内存来存储。zskiplistNode结构体的最后一个元素为柔性数组,申请内存时需要指定柔性数组的大小,一个节点占用的内存大小为zskiplistNode的内存大小与level个zskiplistLevel的内存大小之和。

头节点

头节点是一个特殊的节点,不存储有序集合的member信息。头节点是跳跃表中第一个插入的节点,其level数组的每项forward都为NULL, span值都为0。

创建跳跃表的步骤

创建完头节点后,就可以创建跳跃表。创建跳跃表的步骤如下。

  1. 创建跳跃表结构体对象zsl。
  2. 将zsl的头节点指针指向新创建的头节点。
  3. 跳跃表层高初始化为1,长度初始化为0,尾节点指向NULL。

插入节点

插入节点的步骤:① 查找要插入的位置;② 调整跳跃表高度;③插入节点;④ 调整backward。

调整跳跃表高度

由上文可知,插入节点的高度是随机的,假设要插入节点的高度为3,大于跳跃表的高度2,所以我们需要调整跳跃表的高度。

调整backward

根据update的赋值过程,新插入节点的前一个节点一定是update[0]​,由于每个节点的后退指针只有一个,与此节点的层数无关,所以当插入节点不是最后一个节点时,需要更新被插入节点的backward指向update[0]​。如果新插入节点是最后一个节点,则需要更新跳跃表的尾节点为新插入节点。

删除节点

删除节点的步骤:1)查找需要更新的节点;2)设置span和forward。

设置span和forward

删除节点需要设置update数组中每个节点的span和forward。

删除跳跃表

获取到跳跃表对象之后,从头节点的第0层开始,通过forward指针逐步向后遍历,每遇到一个节点便将释放其内存。当所有节点的内存都被释放之后,释放跳跃表对象,即完成了跳跃表的删除操作。

跳跃表的应用

在Redis中,跳跃表主要应用于有序集合的底层实现(有序集合的另一种实现方式为压缩列表)​。
Redis的配置文件中关于有序集合底层实现的两个配置:

  • zset-max-ziplist-entries 128:zset采用压缩列表时,元素个数最大值。默认值为128。
  • zset-max-ziplist-value 64:zset采用压缩列表时,每个元素的字符串长度最大值。默认值为64。

值得注意的是,zset在转为跳跃表之后,即使元素被逐渐删除,也不会重新转为压缩列表。

小结

跳跃表的原理简单,其查询、插入、删除的平均复杂度都为O(logN)。跳跃表主要应用于有序集合的底层实现。

压缩列表

压缩列表ziplist本质上就是一个字节数组,是Redis为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。

Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。 当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组合。

压缩列表的存储结构

Redis使用字节数组表示一个压缩列表,压缩列表结构示意如图所示:
在这里插入图片描述
图中各字段的含义如下:

  1. zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2^32-1个字节。
  2. zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
  3. zllen:压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535(216-1)的压缩列表,必须遍历整个压缩列表才能获取到元素个数。
  4. entryX:压缩列表存储的元素,可以是字节数组或者整数,长度不限。entry的编码结构将在后面详细介绍。
  5. zlend:压缩列表的结尾,占1个字节,恒为0xFF。

压缩列表元素的编码结构:在这里插入图片描述
previous_entry_length字段表示前一个元素的字节长度,占1个或者5个字节,当前一个元素的长度小于254字节时,用1个字节表示;当前一个元素的长度大于或等于254字节时,用5个字节来表示。而此时previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度。假设已知当前元素的首地址为p,那么p-previous_entry_length就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历。

encoding字段表示当前元素的编码,即content字段存储的数据类型(整数或者字节数组)​,数据内容存储在content字段。为了节约内存,encoding字段同样长度可变。

可以看出,根据encoding字段第1个字节的前2位,可以判断content字段存储的是整数或者字节数组(及其最大长度)​。当content存储的是字节数组时,后续字节标识字节数组的实际长度;当content存储的是整数时,可根据第3、第4位判断整数的具体类型。而当encoding字段标识当前元素存储的是0~12的立即数时,数据直接存储在encoding字段的最后4位,此时没有content字段

结构体

我们发现对于压缩列表的任意元素,获取前一个元素的长度、判断存储的数据类型、获取数据内容都需要经过复杂的解码运算。解码后的结果应该被缓存起来,为此定义了结构体zlentry,用于表示解码后的压缩列表元素。

    typedef struct zlentry {unsigned int prevrawlensize;unsigned int prevrawlen;unsigned int lensize;unsigned int len;unsigned char encoding;unsigned int headersize;unsigned char *p;} zlentry;

回顾压缩列表元素的编码结构,可变因素实际上不止3个:previous_entry_length字段的长度(prevrawlensize)​、previous_entry_length字段存储的内容(prevrawlen)​、encoding字段的长度(lensize)​、encoding字段的内容(len表示元素数据内容的长度,encoding表示数据类型)和当前元素首地址(p)​;而headersize则表示当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和。

连锁更新

压缩列表连锁更新示意如图所示,删除压缩列表zl1位置P1的元素entryX,或者在压缩列表zl2位置P2插入元素entryY时,会出现什么情况呢?
在这里插入图片描述
压缩列表zl1中,由于元素entryX+1长度的增大,元素entryX+2的previous_entry_length字段同样需要改变。依此类推,由于删除了元素entryX,之后的所有元素(entryX+1、entryX+2等)的长度都必须扩展,而每次扩展都将重新分配内存,导致效率很低。压缩列表zl2中,插入元素entryY时同样会出现这种情况,称为连锁更新。从以上分析可以看出,连锁更新会导致多次重新分配内存及数据复制,效率很低。但是出现这种情况的概率是很低的,因此对于删除元素和插入元素操作,Redis并没有为了避免连锁更新而采取措施。

字典

基本概念

字典又称散列表,是用来存储键值(key-value)对的一种数据结构,在很多高级语言中都有实现,如PHP的数组。但是C语言没有这种数据结构,Redis是K-V型数据库,整个数据库是用字典来存储的,对Redis数据库进行任何增、删、改、查操作,实际就是对字典中的数据进行增、删、改、查操作。根据Redis数据库的特点,便可知字典有如下特征:

  • 可以存储海量数据,键值对是映射关系,可以根据键以O(1)的时间复杂度取出或插入关联值。
  • 键值对中键的类型可以是字符串、整型、浮点型等,且键是唯一的。例如:执行set test "hello world"命令,此时的键test类型为字符串,如test这个键存在数据库中,则为修改操作,否则为插入操作。
  • 键值对中值的类型可为String、Hash、List、Set、SortedSet。

数组

当需要对数组a中元素进行操作时,C语言需通过下标找到其对应的内存地址,然后才能对这块内存进行对应的操作。例如,读取a[9]的值, C语言实际上会先转换为*(a+9)的形式,a[9]与*(a+9))这两种形式是等价的,我们对等式两边再取地址,便可得出&a[9]==a+9,也就是说,要得到a[9]的地址,可以通过对数组a的首地址偏移9个元素就行。由此也可以知道,数组根据下标取值时,是通过头指针和偏移量来实现。

通过前文数组介绍可知,​“下标”的含义是数组中第几个元素的意思,只能为整数。根据第2个特征中键的描述:​“键值对中键的类型可以为字符串、整型、浮点型等”​,显然不能直接当成下标使用,此时,需要对键做一些特殊处理,处理过程我们称为Hash。

Hash函数

在应用上,通常使用现成的开源Hash算法,例如Redis自带客户端就是使用“times 33”散列函数来计算字符串的Hash值,Redis服务端的Hash函数使用的是siphash算法,主要功能与客户端Hash函数类似,其优点是针对有规律的键计算出来的Hash值也具有强随机分布性,但算法较为复杂。

那过大的Hash值与较小的数组下标怎么关联呢?最简单的办法是,用Hash值与数组容量取余,会得到一个永远小于数组容量大小的值,此时的值也就恰好可以当作数组下标来使用,我们把取余之后的值称为键在该字典中的索引值,即“索引值==数组下标值”​,拿到“键”的索引值后,我们就知道数组中哪个元素是用来存储键值对中的“值”了。但此方法并不是完美的,还会出现一个问题,Hash冲突。

Hash冲突

为了解决Hash冲突,所以数组中的元素除了应把键值对中的“值”存储外,还应该存储“键”信息和一个next指针,next指针可以把冲突的键值对串成单链表,​“键”信息用于判断是否为当前要查找的键。

Redis字典的实现

Redis字典实现依赖的数据结构主要包含了三部分:字典、Hash表、Hash表节点。字典中嵌入了两个Hash表,Hash表中的table字段存放着Hash表节点,Hash表节点对应存储的是键值对。

Hash表

Hash表,与5.1节设计的字典结构体类似,在Redis源码中取名为Hash表,其数据结构如下:

typedef struct dictht {dictEntry **table;            /*指针数组,用于存储键值对*/unsigned long size;            /*table数组的大小*/unsigned long sizemask;        /*掩码 = size -1 */unsigned long used;            /*table数组已存元素个数,包含next单链表的数据*/
} dictht;

Hash表的结构体整体占用32字节,其中table字段是数组,作用是存储键值对,该数组中的元素指向的是dictEntry的结构体,每个dictEntry里面存有键值对。size表示table数组的总大小。used字段记录着table数组已存键值对个数。

sizemask字段用来计算键的索引值,sizemask的值恒等于size-1。我们知道,索引值是键Hash值与数组总容量取余之后的值,而Redis为提高性能对这个计算进行了优化,具体计算步骤如下:

  • 人为设定Hash表的数组容量初始值为4,随着键值对存储量的增加,就需对Hash表扩容,新扩容的容量大小设定为当前容量大小的一倍,也就是说,Hash表的容量大小只能为4,8,16,32…。而sizemask掩码的值就只能为3,7,15,31…,对应的二进制为11,111,1111,11111…,因此掩码值的二进制肯定是每一位都为1。
  • 索引值=Hash值&掩码值,对应Redis源码为:idx = hash & d->ht[table].sizemask,其计算结果等同Hash值与Hash表容量取余,而计算机的位运算要比取余运算快很多。

Hash表节点

Hash表中的元素是用dictEntry结构体来封装的,主要作用是存储键值对,具体结构体如下:

    typedef struct dictEntry {void *key;                      /*存储键*/union {void *val;                  /*db.dict中的val*/uint64_t u64;int64_t s64;                /*db.expires中存储过期时间*/double d;} v;                            /*值,是个联合体*/struct dictEntry *next;        /*当Hash冲突时,指向冲突的元素,形成单链表*/} dictEntry;

Hash表中元素结构体和我们前面自定义的元素结构体类似,整体占用24字节,key字段存储的是键值对中的键。v字段是个联合体,存储的是键值对中的值,在不同场景下使用不同字段。 例如,用字典存储整个Redis数据库所有的键值对时,用的是*val字段,可以指向不同类型的值;再比如,字典被用作记录键的过期时间时,用的是s64字段存储;当出现了Hash冲突时,next字段用来指向冲突的元素,通过头插法,形成单链表。

字典

Redis字典实现除了包含前面介绍的两个结构体Hash表及Hash表节点外,还在最外面层封装了一个叫字典的数据结构,其主要作用是对散列表再进行一层封装,当字典需要进行一些特殊操作时要用到里面的辅助字段。

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

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

相关文章

Golang学习历程【第五篇 复合数据类型:数组切片】

Golang学习历程【第五篇 复合数据类型&#xff1a;数组&切片】 1. 数组&#xff08;Array&#xff09;1.1 数组的定义1.2 初始化数组1.3 数据的循环遍历1.4 多维数组 2. 切片&#xff08;Slice&#xff09;2.1 切片声明、初始化2.2 基于数组创建切片2.2 切片的长度(len)和容…

【Unity】 HTFramework框架(五十七)通过Tag、Layer批量搜索物体

更新日期&#xff1a;2024年12月30日。 Github源码&#xff1a;[点我获取源码] Gitee源码&#xff1a;[点我获取源码] 索引 问题再现通过Tag搜索物体&#xff08;SearchByTag&#xff09;打开SearchByTag窗口搜索标记指定Tag的所有物体批量修改Tag搜索Undefined状态的所有物体 …

基于feapder爬虫与flask前后端框架的天气数据可视化大屏

# 最近又到期末了&#xff0c;有需要的同学可以借鉴。 一、feapder爬虫 feapder是国产开发的新型爬虫框架&#xff0c;具有轻量且数据库操作方便、异常提醒等优秀特性。本次设计看来利用feapder进行爬虫操作&#xff0c;可以加快爬虫的速率&#xff0c;并且简化数据入库等操作…

PCL点云库入门——PCL库点云滤波算法之统计滤波(StatisticalOutlierRemoval)

1、算法原理 统计滤波算法是一种利用统计学原理对点云数据进行处理的方法。它主要通过计算点云中每个点的统计特性&#xff0c;如均值、方差等&#xff0c;来决定是否保留该点。算法首先会设定一个统计阈值&#xff0c;然后对点云中的每个点进行分析。如果一个点的统计特性与周…

CentOS7 解决ping:www.baidu.com 未知的名称或服务

CentOS7 解决ping&#xff1a;www.baidu.com“未知的名称或服务 在VM查看网络配置 查看虚拟网络编辑器 编辑网络配置文件 vi /etc/sysconfig/network-scripts/ifcfg-ens33注意&#xff1a;不同机器的配置文件名可能不相同&#xff0c;通过 ip addr 命令查看 将 ONBOOT 从 no 改…

aws(学习笔记第二十一课) 开发lambda应用程序

aws(学习笔记第二十一课) 开发lambda应用程序 学习内容&#xff1a; lambda的整体概念开发lambda应用程序 1. lambda的整体概念 借助AWS Lambda&#xff0c;无需预置或管理服务器即可运行代码。只需为使用的计算时间付费。借助 Lambda&#xff0c;可以为几乎任何类型的应用进…

【Leetcode】3280. 将日期转换为二进制表示

文章目录 题目思路代码复杂度分析时间复杂度空间复杂度 结果总结 题目 题目链接&#x1f517; 给你一个字符串 date&#xff0c;它的格式为 yyyy-mm-dd&#xff0c;表示一个公历日期。 date 可以重写为二进制表示&#xff0c;只需要将年、月、日分别转换为对应的二进制表示&a…

docker compose部署kafka集群

先部署zookeeper集群&#xff0c;启动 参考&#xff1a;docker compose部署zookeeper集群-CSDN博客 再部署kafka集群 networks: net: external: true services: kafka1: restart: always image: wurstmeister/kafka:2.13_2.8.1 container_name: kafka1 …

拆解 | 公募REITs:发售上市流程及细节

Hi,围炉喝茶聊产品的新老朋友好,在国庆假期写了两篇有关公募REITs的文章,先简单回顾下,以达到温故知新的效果。 第一篇:一起探索:公募REITs,它从本质、背景、概念等维度较系统介绍了公募REITs,如:明明是“不动产基金”,为什么叫REITs?说到底,投资REITs的实质是什么…

作业:C高级:Day4

第一题 思维导图 第二题 test的指令(整数判断、字符串判断、文件判断) 结果 第三题 题目 终端输入一个C源文件名&#xff08;.c结尾&#xff09;判断文件是否有内容&#xff0c;如果没有内容删除文件&#xff0c;如果有内容编译并执行改文件。 代码 三种情形的结果&#xff1…

(推荐)【通用业务分发架构】1.业务分发 2.rpc调用 3.Event事件系统

一.Reflections和SpringUtil完成扫描包的(反射缓存) 二.id与class的映射泛型上下文(玩家是否登录&#xff0c;rpc调用SeqId&#xff0c;class类名)反射调用 1.netty层的 AccountMsgParam // 登录前 OnlineMsgParam // 登录后 SceneMsgParam // 发到场景层的 2.跨进程rpc调用的…

FristiLeaks_1.3靶场渗透

目录 环境搭建 开始渗透 扫一下存活&#xff08;也就是扫描一下靶机IP&#xff09; ​编辑 扫描端口 扫描服务 查看80端口 扫描一下目录 查看robots 再看一下images,没啥用 回头看主页面&#xff0c;说多喝点FRISTI&#xff0c;我们试试看 尝试admin和万能密码 查看源…

OpenCV的TickMeter计时类

OpenCV的TickMeter计时类 1. TickMeter是一个计时的类1.1 计算耗时1.2 计算循环的平均耗时和FPS1.3 function 2. 案例 1. TickMeter是一个计时的类 https://docs.opencv.org/4.x/d9/d6f/classcv_1_1TickMeter.html#details 1.1 计算耗时 TickMeter tm;tm.start();// do some…

logback之pattern详解以及源码分析

目录 &#xff08;一&#xff09;pattern关键字介绍 &#xff08;二&#xff09;源码分析 &#xff08;一&#xff09;pattern关键字介绍 %d或%date&#xff1a;表示日期&#xff0c;可配置格式化%d{yyyy-MM-dd HH:mm:ss} %r或%relative&#xff1a;也是日期&#xff0c;不过…

【专题】2024年出口跨境电商促销趋势白皮书报告汇总PDF洞察(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p38722 在当今全球化加速演进、数字经济蓬勃发展的大背景下&#xff0c;跨境电商行业正以前所未有的态势重塑国际贸易格局&#xff0c;成为各方瞩目的焦点领域。 根据亚马逊发布的《2024年出口跨境电商促销趋势白皮书》&#xff0c;…

primevue的<Menu>组件

1.使用场景 2.代码 1.给你的menu组件起个引用名 2.<Menu>组件需要一个MenuItem[] 3.你要知道MenuItem[ ]的特殊的数据格式&#xff0c;就像TreeNode[ ]一样&#xff0c;数据格式不对是不渲染的。。。。 常用的属性就这几种&#xff0c;js语言和java不一样&#xff0c;J…

NVR小程序接入平台EasyNVR使用FFmpeg取流时提示错误是什么原因呢?

在视频监控系统中&#xff0c;FFmpeg常用于从各种源&#xff08;如摄像头、文件、网络流等&#xff09;获取流媒体数据&#xff0c;这个过程通常称为“取流”。 在EasyNVR平台中&#xff0c;使用FFmpeg取流是一种常见的操作。FFmpeg作为一款强大的开源多媒体处理工具&#xff…

Springboot 升级带来的Swagger异常

当升级到Springboot 2.6.0 以上的版本后&#xff0c;Swagger 就不能正常工作了, 启动时报如下错误。当然如果你再使用sping boot Actuator 和 Springfox, 也会引起相关的NPE error. (github issue: https://github.com/springfox/springfox/issues/3462) NFO | jvm 1 | 2022/04…

编译原理期末复习-3小时速通

教材使用&#xff1a; 第二章 形式语言理论 基本概念 句子&#xff1a;只包含终结符。&#xff08;基本上就是全部由小写字母组成&#xff09;句型&#xff1a;推导过程中出现的所有符号串都叫做句型。只包含终结符的句型叫做句子。子树&#xff1a;语法树的某个节点连同他向…

【JSTS】JavaScript进阶Typescript秘籍:JS与TS区别?js和ts的语法上的区别?js开发者,如何更好的过渡为ts开发?

JavaScript进阶Typescript秘籍&#xff1a;JS与TS区别&#xff1f;js和ts的语法上的区别&#xff1f;js开发者&#xff0c;如何更好的过渡为ts开发&#xff1f; 一、JavaScript (JS)二、TypeScript (TS)三、JS与TS区别四、js和ts的语法上的区别&#xff1f;js开发者如何更好的过…