认识Redis高性能背后的数据结构(一)

前言

Redis做为一个不少大厂用来解决并发和快速响应的利器,极高的性能让它获得不少公司的青睐,我认为Redis的高性能和其底层的数据结构的设计和实现是分不开的。使用过Redis的同窗可能都知道Redis有五种基本的数据类型:string、list、hash、set、zset;这些只是Redis服务对于客户端提供的第一层面的数据结构。其实内部的数据结构仍是有第二个层面的实现,Redis利用第二个层面的一种或多种数据类型来实现了第一层面的数据类型。我想有和我同样对Redis底层数据结构感兴趣的人,那咱们就一块儿来研究一下Redis高性能的背后的实现底层数据结构的设计和实现。html

这里咱们主要研究的是第二层面的数据结构的实现,其Redis中五种基本的数据类型都是经过如下数据结构实现的,咱们接下来一个一个来看:node

  • sds
  • ziplist
  • quicklist
  • dict
  • skiplist

1. 动态字符串(SDS)

String类型无论是在什么编程语言中都是最多见和经常使用的数据类型,Redis底层是使用C语言编写的,可是Redis没有使用C语言字符串类型,而是自定义了一个Simple Dynamic String (简称SDS)做为Redis底层String的实现,其SDS相比于C语言的字符串有如下优点:git

  • 可动态扩展内存。sds表示的字符串是能够动态扩容的。由于C语言字符串不记录自身的长度,若是改动字符串长度,那就须要从新为新的字符串分配内存,若是不分配内存,可能会产生溢出。可是SDS不须要手动修改内存大小,也不会出现缓冲区溢出问题,由于SDS自己会记录存储的数据大小以及最大的容量,当超过了容量会自动扩容。
  • 二进制安全(Binary Safe)。sds能存储任意二进制数据,不只仅能够存储字符串,还能存储音频、图片、压缩文件等二进制数据。 SDS的api都会以处理二进制的方式来处理存放在buf数组里面,不会对数据作任何限制。
  • 除此以外,sds还兼容了C语言的字符类型。

下面是Redis中SDS的部分源码。sds源码github

typedef char *sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.  * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 {  unsigned char flags; /* 3 lsb of type, and 5 msb of string length */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 {  uint8_t len; /* used */  uint8_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 {  uint16_t len; /* used */  uint16_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 {  uint32_t len; /* used */  uint32_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 {  uint64_t len; /* used */  uint64_t alloc; /* excluding the header and null terminator */  unsigned char flags; /* 3 lsb of type, 5 unused bits */  char buf[]; }; 复制代码

其实咱们能够经过源码发现sds的内部组成,咱们发现sds被定义成了char类型,难道Redis的String类型底层就是char吗?其实sds为了和传统的C语言字符串保持类型兼容,因此它们的类型定义是同样的,都是char *,可是sds不等同是char。web

真正存储数据的是在sdshdr中的buf中,这个数据结构除了能存储字符串之外,还能够存储像图片,视频等二进制数据,。SDS为了兼容C语言的字符串,遵循了C语言字符串以空字符结尾的惯例, 因此在buf中, 用户数据后总跟着一个\0. 即图中 "数据" + "\0" 是为所谓的buf。另外注意sdshdr有五种类型,其实sdshdr5是不使用的,其实使用的也就四种。定义这么多的类型头是为了能让不一样长度的字符串可使用不一样大小的header。这样短字符串就能使用较小的 header,从而节省内存。redis

SDS概览以下图:算法

UTOOLS1591340532574.png
UTOOLS1591340532574.png

除了sdshdr5以外,其它4个header的结构都包含3个字段数据库

  1. len: 表示字符串的真正长度(不包含 \0结束符在内)。
  2. alloc: 表示整个SDS最大容量(不包含 \0字节)。
  3. flags: 老是占用一个字节。其中的最低3个bit用来表示header的类型。header的类型共有5种,用到的也就4种,在sds.h中有常量定义。

2. 列表 list

2.1 底层数据结构

Redis对外暴露的是list数据类型,它底层实现所依赖的内部数据结构其实有几种,在Redis3.2版本以前,链表的底层实现是linkedListzipList,可是在版本3.2以后 linkedListzipList就基本上被弃用了,使用quickList来做为链表的底层实现,ziplist虽然被被quicklist替代,可是ziplist仍然是hash和zset底层实现之一。编程

2.2 压缩链表 zipList 转 双向链表 linkedList

这里咱们使用Redis2.8版本能够看出来,当我插入键 k5 中 110条比较短的数据时候,列表是ziplist编码,当我再往里面插入10000条数据的时候,k5的数据编码就变成了linkedlist。api

UTOOLS1591706020432.png
UTOOLS1591706020432.png

Redis3.2版本以前,list底层默认使用的zipList做为列表底层默认数据结,在必定的条件下,zipList 会转成 linkedList。Redis之因此这样设计,由于双向链表占用的内存比压缩列表要多, 因此当建立新的列表键时, 列表会优先考虑使用压缩列表, 而且在有须要的时候, 才从压缩列表实现转换到双向链表实现。在什么状况下zipList会转成 linkedList,须要知足一下两个任意条件:

  • 这个字符串的长度超过 server.list_max_ziplist_value (默认值为 64 )。
  • ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。

这两个条件是能够修改的,在 redis.conf 中:

list-max-ziplist-value 64 
list-max-ziplist-entries 512 
复制代码

注意:这里列表list的这个配置,只有在Redis3.2版本以前的配置中才能找到,由于Redis3.2和3.2之后的版本去掉了这个配置,由于底层实现不在使用ziplist,而是采用quicklist来做为默认的实现。

2.3 双向链表 linedList

当链表entry数据超过5十二、或单个value 长度超过64,底层就会将zipList转化成linkedlist编码,linkedlist是标准的双向链表,Node节点包含prev和next指针,能够进行双向遍历;还保存了 head 和 tail 两个指针。所以,对链表的表头和表尾进行插入的时间复杂度都为O (1) , 这是也是高效实现 LPUSH 、 RPOP、 RPOPLPUSH 等命令的关键。

2.4 压缩列表 zipList

虽然Redis3.2版本之后再也不直接使用ziplist来实现列表建,可是底层仍是间接的利用了ziplist来实现的。

压缩列表是Redis为了节省内存而开发的,Redis官方对于ziplist的定义是(出自Redis源码中src/ziplist.c注释):

The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values,where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist

翻译:ziplist是一个通过特殊编码的双向链表,它的设计目标就是为了提升存储效率。ziplist能够用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供pushpop操做。

ziplist 将列表中每一项存放在先后连续的地址空间内,每一项因占用的空间不一样,而采用变长编码。当元素个数较少时,Redis 用 ziplist 来存储数据,当元素个数超过某个值时,链表键中会把 ziplist 转化为 linkedlist,字典键中会把 ziplist 转化为 hashtable。因为内存是连续分配的,因此遍历速度很快。

2.4.1 压缩列表的数据结构

ziplist 是一个特殊的双向链表,ziplist没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,经过长度推算下一个元素在什么地方,牺牲读取的性能,得到高效的存储空间,这是典型的"时间换空间"。

ziplist使用连续的内存块,每个节点(entry)都是连续存储的;ziplist 存储分布以下:

UTOOLS1591627618164.png
UTOOLS1591627618164.png

每一个字段表明的含义。

  • zlbytes: 32bit,表示ziplist占用的字节总数(也包括 zlbytes自己占用的4个字节)。
  • zltail: 32bit,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。 zltail的存在,使得咱们能够很方便地找到最后一项(不用遍历整个ziplist),从而能够在ziplist尾端快速地执行push或pop操做。
  • zllen: 16bit, 表示ziplist中数据项(entry)的个数。zllen字段由于只有16bit,因此能够表达的最大值为2^16-1。这里须要特别注意的是,若是ziplist中数据项个数超过了16bit能表达的最大值,ziplist仍然能够来表示。那怎么表示呢?这里作了这样的规定:若是 zllen小于等于2^16-2(也就是不等于2^16-1),那么 zllen就表示ziplist中数据项的个数;不然,也就是 zllen等于16bit全为1的状况,那么 zllen就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头至尾遍历各个数据项,才能计数出来。
  • entry: 表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它本身的内部结构。
  • zlend: ziplist最后1个字节,是一个结束标记,值固定等于255。

2.4.2 zipList节点entry结构

ziplist每个存储节点、都是一个 zlentry。zlentry的源码在ziplist.c 第 268行

/* We use this function to receive information about a ziplist entry.  * Note that this is not how the data is actually encoded, is just what we  * get filled by a function in order to operate more easily. */ typedef struct zlentry {  unsigned int prevrawlensize; /* prevrawlensize是指prevrawlen的大小,有1字节和5字节两种*/  unsigned int prevrawlen; /* 前一个节点的长度 */  unsigned int lensize; /* lensize为编码len所需的字节大小*/  unsigned int len; /* len为当前节点长度*/  unsigned int headersize; /* 当前节点的header大小 */  unsigned char encoding; /*节点的编码方式:ZIP_STR_* or ZIP_INT_* */  unsigned char *p; /* 指向节点的指针 */ } zlentry; 复制代码
  • prevrawlen:记录前一个节点所占有的内存字节数,经过该值,咱们能够从当前节点计算前一个节点的地址,能够用来实现表尾向表头节点遍历;prevrawlen是变长编码,有两种表示方法
    • 若是前一节点的长度小于 254 字节,则使用1字节(uint8_t)来存储prevrawlen;
    • 若是前一节点的长度大于等于 254 字节,那么将第 1 个字节的值设为 254 ,而后用接下来的 4 个字节保存实际长度。
  • len/encoding:记录了当前节点content占有的内存字节数及其存储类型,用来解析content用;
  • content:保存了当前节点的值。 最关键的是prevrawlen和len/encoding,content只是实际存储数值的比特位。

2.4.3 为何zipList 能够作到数据压缩

由于ziplist采用了一段连续的内存来存储数据,减小了内存碎片和指针的内存占用。其次表中每一项存放在先后连续的地址空间内,每一项因占用的空间不一样,而采用变长编码,并且当节点较少时,ziplist更容易被加载到CPU缓存中。这也是ziplist能够作到压缩内存的缘由。

2.4.4 为何zipList被舍弃了

经过上面咱们已经清楚的了解的ziplist的数据结构,在ziplist中每一个zlentry都存储着前一个节点所占的字节数,而这个数值又是变长的,这样的数据结构可能会引发ziplist的连锁更新。假设咱们有一个压缩链表 entry1 entry2 entry3 .......,entry1的长度正好是 253个字节,那么按照咱们上面所说的,entry2.prevrawlen 记录了entry1的长度,使用1个字节来保存entry1的大小,假如如今在entry1 和 entry2之间插入了一个新的 new_entry节点,而new_entry的大小正好是254,那此时entry2.prevrawlen就须要扩充为5字节;若是entry2的总体长度变化又引发了entry3.prevrawlen的存储长度变化,如此连锁的更新直到尾结点或者某一个节点的prevrawlen足以存放以前节点的长度,固然删除节点也是一样的道理,只要咱们的操做的节点以后的prevrawlen发生了改变就会出现这种连锁更新。

因为ziplist连锁更新的问题,也使得ziplist的优缺点极其明显;ziplist被设计出来的目的是节省内存,这种结构并不擅长作修改操做。一旦数据发生改动,就会引起内存从新分配,可能致使内存拷贝。也使得后续Redis采起折中,利用quicklist替换了ziplist。

2.5 快速列表 quickList

基于上面所说,咱们已经知道了ziplist的缺陷,因此在Redis3.2版本之后,列表的底层默认实现就使用了quicklist来代替ziplist和linkedlist?接下来咱们就看一下quicklist的数据结构是什么样的,为何使用quicklist做为Redis列表的底层实现,它的优点相比于ziplist优点在哪里,接下来咱们就一块儿来看一下quicklist的具体实现。下面是我基于Redis3.2的版本作的操做,这里咱们能够看到列表的底层默认的实现是quicklist对象编码。

UTOOLS1591706621114.png
UTOOLS1591706621114.png

2.5.1 quicklist数据结构

quicklist总体的数据结构以下:

UTOOLS1591872656750.png

quicklist源码 redis/src/quicklist.h结构定义以下:

typedef struct quicklist {
 quicklistNode *head; // 头结点  quicklistNode *tail; // 尾结点   unsigned long count; // 全部ziplist数据项的个数总和  unsigned long len; //quicklistNode的节点个数  int fill : QL_FILL_BITS; //ziplist大小设置,经过配置文件中list-max-ziplist-size参数设置的值。  unsigned int compress : QL_COMP_BITS; //节点压缩深度设置,经过配置文件list-compress-depth参数设置的值。  unsigned int bookmark_count: QL_BM_BITS;  quicklistBookmark bookmarks[]; } quicklist; 复制代码

其实就算使用的quicklist结构来代替ziplist,那quicklist也是有必定的缺点,底层仍然使用了ziplist,这样一样会有一个问题,由于ziplist是一个连续的内存地址,若是ziplist过小,就会产生不少小的磁盘碎片,从而下降存储效率,若是ziplist很大,那分配连续的大块内存空间的难度也就越大,也会下降存储的效率。如何平衡ziplist的大小呢?那这样就会取决于使用的场景,Redis提供了一个配置参数list-max-ziplist-size能够调整ziplist的大小。

当取正值的时候,表示按照数据项个数来限定每一个quicklist节点上的ziplist长度。好比,当这个参数配置成5的时候,表示每一个quicklist节点的ziplist最多包含5个数据项。当取负值的时候,表示按照占用字节数来限定每一个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,每一个值含义以下:

  • -5: 每一个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
  • -4: 每一个quicklist节点上的ziplist大小不能超过32 Kb。
  • -3: 每一个quicklist节点上的ziplist大小不能超过16 Kb。
  • -2: 每一个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
  • -1: 每一个quicklist节点上的ziplist大小不能超过4 Kb。

当咱们数据量很大的时候,最方便访问的数据基本上就是队列头和队尾的数据(时间复杂度为O(1)),中间的数据被访问的频率比较低(访问性能也比较低,时间复杂度是O(N),若是你的使用场景符合这个特色,Redis为了压缩内存的使用,提供了list-compress-depth这个配置可以把中间的数据节点进行压缩。quicklist内部节点的压缩算法,采用的LZF——一种无损压缩算法。

这个参数表示一个quicklist两端不被压缩的节点个数。参数list-compress-depth的取值含义以下:

  • 0: 是个特殊值,表示都不压缩。这是Redis的默认值。
  • 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
  • 2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
  • 3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
  • 依此类推

2.5.2 quicklistNode结构

quicklist是由一个个quicklistNode的双向链表构成。

typedef struct quicklistNode {
 struct quicklistNode *prev;  struct quicklistNode *next;  unsigned char *zl;  unsigned int sz; /* ziplist size in bytes */  unsigned int count : 16; /* count of items in ziplist */  unsigned int encoding : 2; /* RAW==1 or LZF==2 */  unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */  unsigned int recompress : 1; /* was this node previous compressed? */  unsigned int attempted_compress : 1; /* node can't compress; too small */  unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode;  typedef struct quicklistLZF {  unsigned int sz;  char compressed[]; } quicklistLZF;  复制代码

quicklist中的每一个节点都是一个quicklistNode,其中各个字段的含义以下:

  • prev: 指向链表前一个节点的指针。
  • next: 指向链表后一个节点的指针。
  • zl:数据指针。若是当前节点的数据没有压缩,那么它指向一个ziplist结构;不然,它指向一个quicklistLZF结构。
  • sz:表示zl指向的ziplist的总大小,须要注意的是:若是ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小。
  • count: 表示ziplist里面包含的数据项个数。
  • encoding:表示ziplist是否压缩了(以及用了哪一个压缩算法)。目前只有两种取值:2表示被压缩了(并且用的是 LZF压缩算法),1表示没有压缩。
  • container:在目前的实现中,这个值是一个固定的值2,表示使用ziplist做为数据容器。
  • recompress: 当咱们使用相似lindex这样的命令查看了某一项压缩以前的数据时,须要把数据暂时解压,这时就设置recompress = 1作一个标记,等有机会再把数据从新压缩。
  • attempted_compress:这不太理解其含义。
  • extra:扩展预留字段。

quicklist结构结合ziplist和linkedlist的优势,quicklist权衡了时间和空间的消耗,很大程度的优化了性能,quicklist由于队头和队尾操做的时间复杂度都是O(1),因此Redis的列表也能够被做用队列来使用。

3. 字典 dict

UTOOLS1592647523856.png
UTOOLS1592647523856.png

经过上图咱们可以看到hash键的底层默认实现的数据结构是ziplist,随着hash键的数量变大时,数据结构就变成了hashtable,虽然这里的咱们看到的对象编码格式hashtable,可是Redis底层是使用字典dict来完成了Hash键的底层数据结构,不过字典dict的底层实现是使用哈希表来实现的。Redis服务对于客户端来讲,对外暴露的类型是hash,其底层的数据结构实现有两种,一种是压缩列表(ziplist),另一种则是字典(dict);关于ziplist的,咱们在说链表(list)的时候已经说过了,这里不重复去说了。咱们这里就着重的去看一下字典(dict)的具体实现。

这里仍是要说一下什么状况下会从ziplist转成hashtable呢?redis.conf中提供了两个参数

hash-max-ziplist-entries 512
hash-max-ziplist-value 64 复制代码
  • 表示当hash键中key所对应的项(field,value)数>512 时候转为字典。
  • 表示当hash键中key所对应的value长度超过64的时候转为字典

3.1 字典(dict)的实现

字典算是Redis比较重要的一个数据结构了,Redis数据库自己就能够当作是一个大的字典,Redis之因此会有很高的查询效率,其实和Redis底层使用的数据类型是有关系的,一般字典的实现会用哈希表做为底层的存储,redis的字典实现也是基于时间复杂度为O(1)的hash算法。

Redis源码其结构定义以下:dict源码定义

typedef struct dictEntry {
 void *key;  union {  void *val;  uint64_t u64;  int64_t s64;  double d;  } v;  struct dictEntry *next; } dictEntry;  typedef struct dictType {  uint64_t (*hashFunction)(const void *key);  void *(*keyDup)(void *privdata, const void *key);  void *(*valDup)(void *privdata, const void *obj);  int (*keyCompare)(void *privdata, const void *key1, const void *key2);  void (*keyDestructor)(void *privdata, void *key);  void (*valDestructor)(void *privdata, void *obj); } dictType;  /* This is our hash table structure. Every dictionary has two of this as we  * implement incremental rehashing, for the old to the new table. */ typedef struct dictht {  dictEntry **table;  unsigned long size;  unsigned long sizemask;  unsigned long used; } dictht;  typedef struct dict {  dictType *type;  void *privdata;  dictht ht[2];  long rehashidx; /* rehashing not in progress if rehashidx == -1 */  unsigned long iterators; /* number of iterators currently running */ } dict; 复制代码

下图能更清晰的展现dict的数据结构。

UTOOLS1593064857861.png
UTOOLS1593064857861.png

经过上图和源码咱们能够很清晰的看到,一个字典dict的构成由下面几项构成:

  1. dictType: 一个指向dictType结构的指针(type),经过自定义的方式使得dict的key和value可以存储任何类型的数据。
  2. privdate: 一个私的指针,由调用者在建立dict的时候传进来。
  3. dictht[2]: 一个hash表数组,且数组的大小是2。只有在重哈希的过程当中,ht[0]和ht[1]才都有效。而在日常状况下,只有ht[0]有效,ht[1]里面没有任何数据。
  4. rehashidex: 当前重哈希索引(rehashidx)。若是rehashidx = -1,表示当前没有在重哈希过程当中;不然,表示当前正在进行重哈希,且它的值记录了当前重哈希进行到哪一步了。
  5. iterators: 当前正在进行遍历的iterator的个数。

这里最重要的仍是dictht这个结构,dictht定义了一个哈希表,其结构由如下组成:

  1. dictEntry:一个dictEntry指针数组(table)。key的哈希值最终映射到这个数组的某个位置上(对应一个bucket)。若是多个key映射到同一个位置,就发生了冲突,那么就拉出一个dictEntry链表。熟悉Java的同窗看到这里可能会想到HashMap,这里其实和HashMap的实现有些相像。
  2. size:标识dictEntry指针数组的长度。它老是2的指数。
  3. sizemask:用于将哈希值映射到table的位置索引。它的值等于(size-1),也就是数组的下标。这里其实和HashMap中计算索引的方法是同样的。
  4. used:记录dict中现有的数据个数。它与size的比值就是装载因子(load factor)。这个比值越大,哈希值冲突几率越高。

3.2 Redis中dict如何进行rehash的

总体看下来,有点相似于Java中HashMap的实现,在处理哈希冲突和数组的大小都是和Java中的HashMap是同样的,可是这里有一点不同就是关于扩容的机制,Redis这里利用了两个哈希表,另一个哈希表就是扩容用的。Redis中的字典和Java中的HashMap同样,为了保证随着数据量增大致使查询的效率问题,要适当的调整数组的大小,也就是rehash,也就是咱们熟知扩容。咱们这里不说Java中的HashMap的扩容了,这里主要看一下Redis中对于字典的扩容。

那么何时才会rehash呢?条件: 1. 服务器目前没有执行的BGSAVE命令或者BGREWRUTEAOF命令,而且哈希表的负载因子大于等于1; 2. 服务器目前正在执行BGSAVE命令或者BGREWRUTEAOF命令,而且哈希表的负载因子大于等于5;

那究竟是如何进行rehash的,根据上面源码和数据结构图能够看到,字典中定义一个大小为2的哈希表数组,前面咱们也说到了,在不进行扩容的时候,全部的数据都是存储在第一个哈希表中,只有在进行扩容的时候才会用到第二个哈希表。当须要进行rehash的时候,将dictht[1]的哈希表大小设置为须要扩容以后的大小,而后将dictht[0]中的全部数据从新rehash到dictht[1]中;并且Redis为了保证在数据量很大的状况rehash不太过消耗服务器性能,其采用了渐进式rehash,当数据量很小的时候咱们一次性的将数据从新rehash到扩容以后的哈希表中,对Redis服务的性能是能够忽略不计的,可是当Redis中hash键的数量很大,几十万甚至上百万的数据时,这样rehash对Redis带来的影响是巨大的,甚至会致使一段时间内Redis中止服务,这是不能接受的。

Redis服务在须要rehash的时候,不是一次性将dictht[0]中的数据所有rehash到dictht[1]中,而是分批进行依次将数据从新rehash到dictht[1]的哈希表中。这就是采用了分治的思想,就算在数据量很大的时候也能避免集中式rehash带来的巨大计算量。当进行rehash的期间,对字典的增删改查都会操做两个哈希表,由于在进行rehahs的时候,两个哈希表都有说句,当咱们在一个哈希表中查找不到数据的时候,也会去另外一个哈希表查数据。在rehash期间的新增,不会在第一个哈希表中新增,会直接把新增的数据保存到第二个哈希表中这样能够确保第一个哈希表中的数据只减不增,直到数据为空结束rehash。

熟悉Java的同窗可能会想起HashMap中扩容算法,其实包括从容量的设计上和内部的结构都有不少类似的地方,有兴趣的同窗能够去了解一下,也能够参考我写的这篇文章《Java1.8中HashMap的骚操做》,相比于Redis中的字典的rehash的方式,我更喜欢的是Java中对于HashMap中精妙的rehahs的方式,其思想仍是很是值得咱们去借鉴的。

相关文章
相关标签/搜索