Redis数据结构之字典

sherlock_lin 2020-11-07 23:59:07
redis 数据结构 字典 数据 结构


1、说明

当我们使用 Redis 的 Hash 操作时,底层的实现就是字典。

在介绍字典之后,我们先回忆一下 Redis 中的 Hash 操作。最常用的就是 HSETHGET

127.0.0.1:6379> HSET user name sherlock
(integer) 1
127.0.0.1:6379> HSET user age 20
(integer) 1
127.0.0.1:6379> HGET user name
"sherlock"
127.0.0.1:6379> HGET user age
"20"
127.0.0.1:6379>

除了 HSETHGET 外的常见指令还有:HDEL、HEXISTS、HGETALL、HMGET 等等,这里就不一一列举了,Redis 的 Hash 操作一般都是以 H 开头的。

我们可以看到,Hash 操作可以保存很多组键值对,其底层的视线就是字典

2、dict

字典的定义在源码目录下 src/dict.h 文件中,为了便于理解,我们从最基本的结构往上介绍

2.1、dictEntry

首先是 dictEntry,它表示字典中的一组键值对,声明如下:

typedef struct dictEntry {
void *key; //键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; //值
struct dictEntry *next; //指向下个键值对
} dictEntry;
  • key 表示的键值对的键;

  • v 表示键值对的值,v 是一个共同体,表示这里的值类型可以是指针、uint64_t、int64_t 和 double 其中之一,用共同体可以节约内存;

  • dictEntrynext 指向下一组键值对,这里是链表,当需要存储的键值对最后计算得到的存储的位置索引出现重复的时候,就使用链表,将多个键值对存在一个数组元素中,而且,Redis 中,新数据会存储在链表的最前面

2.2、dictht

dictht 即为 Redis 操作时的值结构,用于保存多组的键值对,声明如下:

typedef struct dictht {
dictEntry **table; //键值对数组,数组的元素是个链表
unsigned long size; //键值对数组的大小
unsigned long sizemask; //掩码,用于计算索引
unsigned long used; //键值对数量
} dictht;
  • table 是一个 dictEntry 类型的数组指针,它的每个元素都是指针,都指向一个 dictEntry 类型;
  • size 表示键值对数组的大小;
  • sizemask 为掩码,用于计算键值对插入时的数组索引,它总是 size - 1,后面会再次说到;
  • used 表示当前哈希表存储的键值对的数量;

下图是一个 dictht 的存储结构,k0和k1的键值计算的索引相同,所以放在一个数组元素中:

image-20201107211840121

2.3、dict

dict 是最终的字典的数据结构,声明如下:

typedef struct dictType {
unsigned int (*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;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
  • dictType 是一组操作函数指针,用于操作特定类型的键值对,Redis 会为不同类型的键值对设置不同类型的函数;

  • privdata 表示需要传递给操作函数的特定私有数据;

  • ht 是一个 dictht 类型的数组,有两个元素,之所以两个,是因为需要rehash,后面会再次说到;

  • rehashidx 表示 rehash 进度,-1表示当前并没有进行 rehash;

  • iterators 表示当前运行的迭代器数量,本次不做特别说明;

下图表示一个没有在rehash的字典

image-20201107212436924

3、字典的存储方法

Redis 中的哈希操作,顾名思义,存储方法肯定和哈希算法有关,这里先简单介绍一下哈希算法。

3.1、哈希算法

哈希算法又称之为散列函数,它把任意长度的输入,通过一系列的算法, 变换成固定长度的输出。

Redis 底层使用的哈希算法是 MurmurHash 算法,最初由 Austin Appleby 在2008 年发明,其优势在于,无论输入是否有规律,输出都是随机分布的,而且速度很快。

其实了解哈希的同学都能够明白,用有限的hash值来表示无限的数据,肯定会出现不同的数据得出重复的哈希值的问题,当然,专业的说法不叫重复,叫 碰撞。

3.2、存储过程和键冲突

回到正题,现在当一个键值对添加到字典中,会先计算出当前键值对需要存储在 table 中的 index,计算分两步,先根据键计算hash值,再根据hash值和掩码计算index

hash = dict->type->hashFunction(key); //根据键计算其hash值
//根据hash值和掩码计算索引,ht[x]可能会是h[0],也可能是h[1],根据rehash来定
index = hash & dict->ht[x].sizemask;

首先,对于不同的key,这里计算的hash值可能相同,其次,不同的hash值经过和掩码取&,会出现相同的index,这也是使用链表的原因,在 Redis 中称之为键发生了冲突(collision)

我们前面说到,sizemask 总是 size - 1,即数组的最大索引,hash & sizemask 就保证得出的 index一定是一个小于等于 sizemask 的值,即一定在数组内

计算错 index 之后,就把该键值对保存到 table 对应的位置,table 的元素都是链表,新插入的键值对,会保存在链表的最前端,这样效率最高,时间复杂度为 O(1),如果保存在最后端,那么时间复杂度为O(N)。反正放在最前端和最后端都一样,就取一个插入最快的吧。

这样存储完成之后,我们取数据也就是要在数组和链表中取,时间复杂度也就是 O(1) + O(N),这里的N越小越好,即链表越小越好,最好没有键冲突,那么时间复杂度就是O(1)

3.3、rehash

rehash 即对hash表进行扩展和收缩。

这个操作是非常又必要的,可以想象一下,假设一开始创建的 dictEntry 数组的大小只有100个,结果随着时间的推移,保存的键值对慢慢变多,变成五六百个,那么,键冲突的概率就会成倍地增加,最后就会导致个别数组内的链表元素有多个,这样就大大地增加了读取的效率,此时就很有必要对原先只有100个元素的数组进行拓展,比如扩展成五六百个,尽量保证,链表节点的数量最小。同理,当保存的键值对删减之后,缩小数组可以节约内存,反正空着也是空着,不如释放了。

提到hash,就不得不提负载因子(load factor)的概念,它是保存的键值对和数组大小的一个比值,使用扩展和收缩的手段,把它控制在一个合理的范围之内,可以避免内存的浪费和读取的低效

负载因子的计算公式如下:

load_factor = ht[0].used / ht[0].size

used 是保存的的键值对的数目,size 是数组的大小

可见,如果负载因子太大,表示数组中的元素的链表元素会多,即键冲突的概率会变大;

而如果负载因子太小,表示数组中可能有些元素没有使用,即有些内存浪费了;

哈希表的收缩和扩展:

Redis 中,当满足下面两个条件之一时,会自动进行扩展操作:

  1. 当服务器没有执行 BGSAVEBGREWRITEAOF 命令,并且负载因子大于等于1的时候;
  2. 当服务器正在执行 BGSAVEBGREWRITEAOF 命令,并且负载因子大于等于5的时候;

这是因为这两个命令在执行过程中,Redis 需要创建子进程,而大多数的操作系统都采用写时复制的技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高触发扩展所需的负载因子,尽可能避免在子进程存在期间进行扩展操作,最大限度地节约内存。

当 Redis 进行 rehash 这种操作的时候,客户端还在使用,要兼顾 rehash 和 客户端,就要保证原数据和新数据同时存储和查询。这就是 dict 结构中 ht 大小为2的原因。

在没有进行 rehash 的时候,只使用 ht[0],rehash 会将新旧数据都重新散列,存入 ht[1] 中。rehash 完成之后,将 ht[1] 变成 ht[0],原来的 ht[0] 释放掉,再新建一个 ht[1]

3.4、渐进式rehash

当数据量很大的时候,rehash 操作如果一次性将h[0]数据转到ht[1],会导致服务宕机,这是不能接受的。因此,Redis 的 rehash 操作并不是一次性、集中式地完成,而是分多次、渐进式地完成的。

大致步骤如下:

  1. 为 ht[1] 分配空间,,让字典同时持有 ht[0] 和 ht[1] 两个哈希表;
  2. 将 rehashindex 值设置为0,表示开始 rehash;
  3. 在 rehash 期间,每次对字典执行增删查改的操作时,程序还会顺带将 ht[0] 中哈希表在 rehashindex 上的所有键值对 rehash 到 ht[1] 上,完成只有,rehashindex 递增;
  4. 随着字典的操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被rehash至 ht[1],这时候,rehash 全部完成,将 rehashindex 设置为-1;

渐进式 rehash 的好处在于,其采用分治的方式,将 rehash 键值对的工作量均摊到每次对字典的增删查改上,避免了集中式 rehash 带来的庞大的计算量

渐进式 rehash 执行期间的哈希表操作:

rehash 的时候,ht[0] 和 ht[1] 同时使用,字典的增删查改等操作会先后在 ht[0] 和 ht[1] 上执行,比如查找时,先在 ht[0] 上查找,如果没有找到,则在 ht[1] 上查找。

rehash 的时候,ht[0] 只删改查,不会进行添加操作,添加会直接添加到 ht[1] 中,保证了 ht[0] 中的键值对数量只减不增,直到 rehash 完成之后,ht[0] 变成空表。

4、总结

  1. 字典广泛用于实现 Redis 的各种功能,包括数据库和哈希键;
  2. 字典底层使用哈希表实现,每个字典带有两个哈希表,一个平时使用,一个 rehash 的时候使用;
  3. 哈希表使用单项链表来解决键冲突;
  4. 哈希表扩展和收缩时,Redis 会将一个哈希表 rehash 到另一个哈希表上,这个操作是渐进式的,不是一次完成的;
版权声明
本文为[sherlock_lin]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/sherlock-lin/p/13942995.html

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云