字典

字典又称为符号表、关联数组或映射(map),是一种用于保存键值对(key-value)的数据结构。

那么 C 语言中有没有这样 key-value 型的内置数据结构呢? 答案:没有。

说起键值对,是不是想到了 Java 中的 Map?Java中的 Map 实现有两个:HashMap 和 TreeMap。

HashMap的底层是 hash 表,TreeMap 的底层是二叉搜索树,而 Redis 必须要求的一点就是效率,所以 Redis 中的字典使用的是 hash 表。

那么下面我们就拿 Redis 中的字典与 Java 中的 HashMap 对比一下

  • Redis 定义的字典结构是什么?Java 的呢?
  • Java 和 Redis 的字符串结构有什么区别?

Redis 中字典的数据结构是什么?

Redis 中字典的底层使用的 hash 表,它的具体结构如下:

    /*
* 字典
*
* 每个字典使用两个哈希表,用于实现渐进式 rehash
*/
typedef struct dict { // 特定于类型的处理函数
dictType *type; // 类型处理函数的私有数据
void *privdata; // 哈希表(2 个)
dictht ht[2]; // 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行,否则表示 rehash 进行到了 ht[0] 具体索引上
int rehashidx; } dict;
/*
* 哈希表
*/
typedef struct dictht { // 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table; // 指针数组的大小
unsigned long size; // 指针数组的长度掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask; // 哈希表现有的节点数量
unsigned long used; } dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry { // 键
void *key; // 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v; // 链往后继节点
struct dictEntry *next; } dictEntry;

哈希表的基本结构这里就不再解释了,不知道的可以百度一下。这里解释一下 dict 中的内容,如下(出自《Redis设计与实现第二版》第四章:字典):

Java 中 HashMap 的具体实现就不多说了,结构和上面不一样。

Java 中 HashMap 和 redis 中字典的比较

Hash 算法的比较

当要将一个新的键值对添加到字典里面时,需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引下面。

Redis 是通过 MurmurHash2 算法求出 key 的 hash 值;Java 是通过引入 hashCode 的概念计算 hash 值。

解决 hash 冲突的方法

当有两个或以上的键值对被分配到 hash 表数组的同一个索引上面时,即发生了 hash 冲突。那么 Redis 如何解决的 hash 冲突呢?

这就用到 dictEntry 结构的 next 节点了,通过 next 将 hash 表节点串起来。例:k1-v1 和 k2-v2 分配到了同一个索引下,示意图如下(出自《Redis设计与实现第二版》第四章:字典):

所以,理论上来说在特殊情况下 hash 会退化成链表。而 Java 为了解决 hash 表退化的问题,在 JDK1.8 的 HashMap 中引入了红黑树。

扩容与收缩(rehash)

随着操作的不断进行,哈希表保存的键值对会逐渐的增多或减少,如果hash表保存的键值对过多,会影响查询的效率;反之,如果过小,又会浪费空间。所以程序就需要对哈希表的大小进行相应的扩容或者收缩。

无论扩容还是收缩,都涉及到三个问题:

(1)触发条件是什么?是手动触发还是自动触发。

(2)扩容或者收缩的规则是什么?具体过程是怎样的?

(3)当哈希表在扩容时,是否允许别的线程来操作这个哈希表?

** Redis 的实现**

Redis 中的扩展和收缩是通过 rehash 操作完成的,扩容的触发条件是:

  1. 当服务器没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令时,负载因子大于等于 1 时触发。
  2. 当服务器正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令时,负载因子大于等于 5 时触发。

其中:哈希表的负载因子 = 哈希表已保存节点数量 / 哈希表总长度。

收缩的条件是:哈希表的负载因子 < 0.1。

扩容的规则是:扩容至第一个大于等于 ht[0].used(当前包含键值对数量)* 2 的 2^n;

收缩的规则是:收缩至第一个大于等于 ht[0].used(当前包含键值对数量)* 2 的 2^n;

扩容和收缩的过程一样,都是:

  1. 为字典的 ht[1] 哈希表分配空间。
  2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上。
  3. 释放 ht[0] ,ht[1] = ht[0],再为 ht[1] 新创建一个空的哈希表。

当 Redis 中的哈希表在扩容时,必须得能允许别的请求来操作哈希表。否则一旦要rehash一个很大的哈希表时,就得拒绝所有对该哈希表的请求,这是不被允许的。所以 Redis 在上面的第二步(rehash)是渐进式 rehash 的。rehash的详细步骤如下(出自《Redis设计与实现第二版》第四章:字典):

因为在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[l] 两个哈希表,所以在渐进式 rehash 进行期间,字典的删除(delete)、査找(find)、更新(update)等操作,会在两个哈希表上进行。例如,要在字典里面査找一个键的话,程序会先在ht[0]里面进行査找,如果没找到的话,就会继续到ht[l]里面进行査找,诸如此类。

另外,在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[l] 里面, 而 ht[0] 则不再进行任何添加操作,这一措施保证了 ht[0] 包含的键值对数量会只减不 增,并随着 rehash 操作的执行而最终变成空表。

渐进式 rehash 的好处在于既将哈希表的扩容操作变成了线程安全的,又把计算量分摊到了对字典的每个增删改查操作上面。

上期思考问题

上一篇遗留的问题:

Redis 中的 SDS(Simple Dynamic String)、Java中的 (StringBuilder、StringBuffer、ArrayList ),他们的扩容规则分别是什么呢?

Redis 中的 SDS 的扩容规则是:

  • 如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于1M,那么程序分配和 len 属性同样大小的未使用空间,这时 SDS len 属性的值将和 free 属性的值相同。举个例子,如果进行修改之后, SDS 的 len 将变成 13 字节,那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
  • 如果对 SDS 进行修改之后, SDS 的长度将大于等于 1MB ,那么程序会分配 1M 的未使用空间。举个例子,如果进行修改之后,SDS 的 len 将变成 30MB,那么程序会分配 1M 的未使用空间,SDS 的 buf 数组的实际长度将为 30MB + 1MB + 1byte。

Java中:

  • StringBuilder 和 StringBuffer 都继承了 AbstractStringBuilder,扩容规则都是:扩容至 newCapacity 和 oldCapacity * 2 + 2 的最大值。部分代码如下:

      private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
    newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
    ? hugeCapacity(minCapacity)
    : newCapacity;
    }
  • ArrayList:如果容量允许的情况下扩容至 newCapacity 和 oldCapacity + oldCapacity / 2 的最大值。 部分代码如下:

      private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
    }

本期思考问题:

本篇介绍了 Redis 中有关扩容和收缩的3个问题,那么对应的 Java 中 HashMap 关于扩容和收缩的问题又是怎么处理的呢?

Redis 学习笔记(篇二):字典的更多相关文章

  1. Redis学习笔记(二)-key相关命令【转载】

    转自 Redis学习笔记(二)-key相关命令 - 点解 - 博客园http://www.cnblogs.com/leny/p/5638764.html Redis支持的各种数据类型包括string, ...

  2. Redis学习笔记(二)Redis支持的5种数据类型的总结之String和Hash

    引言 在Redis学习笔记(一)中我们已经会安装并且简单使用Redis了,接下来我们一起来学习下Redis支持的5大数据类型. 简介 Redis是REmote DIctionary Server(远程 ...

  3. Redis学习笔记(二) Redis 数据类型

    Redis 支持五种数据类型:string(字符串).list(列表).hash(哈希).set(集合)和 zset(有序集合),接下来我们讲解分别讲解一下这五种类型的的使用. String(字符串) ...

  4. Redis学习笔记(二十一) 事务

    文章开始啰嗦两句,写到这里共21篇关于redis的琐碎知识,没有过多的写编程过程中redis的应用,着重写的是redis命令.客户端.服务器以及生产环境搭建用到的主从.哨兵.集群实现原理,如果你真的能 ...

  5. Redis学习笔记(二十) 发布订阅(下)

    当一个客户端执行SUBSCRIBE命令订阅某个或某些频道时,这个客户端与被订阅频道之间就建立起了一种订阅关系. Redis将所有频道的订阅关系保存在服务器状态的pubsub_channels字典里面, ...

  6. Redis学习笔记(二)redis 底层数据结构

    在上一节提到的图中,我们知道,可以通过 redisObject 对象的 type 和 encoding 属性.可以决定Redis 主要的底层数据结构:SDS.QuickList.ZipList.Has ...

  7. Redis学习笔记之二 :在Java项目中使用Redis

    成功配置redis之后,便来学习使用redis.首先了解下redis的数据类型. Redis的数据类型 Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set( ...

  8. Redis学习笔记(二) 链表

    链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度. redis中链表应用广泛,如list中就使用了链表. 每一个链表节点使用listNode结构标识( ...

  9. Redis学习笔记(二)

    解读Retwis官网例子 Redis需要考虑需要哪些keys以及对应的value使用合适的数据类型进行存储.在retwis例子中,我们需要users,user的粉丝列表, user的关注用户列表等等. ...

  10. Redis学习笔记(二) ---- PHP操作Redis各数据类型

    Redis 一.使用PHP操作Redis存储系统中的各类数据类型方法 1.String(字符串)操作 <?php // 1. 实例化 $redis = new Redis; // 2. 连接 r ...

随机推荐

  1. SQLyog 报错2058 :连接 mysql 8.0.12 解决方法

    今天闲来无事,下载新版的 mysql 8.0.12 安装. 为了方便安装查看,我下载了sqlyog 工具 连接 mysql 配置新连接报错:错误号码 2058,分析是 mysql 密码加密方法变了. ...

  2. WPF实现抽屉效果

    原文:WPF实现抽屉效果 界面代码(xaml): <Window x:Class="TransAnimation.MainWindow" xmlns="http:/ ...

  3. 张忠谋:3nm制程会出来 2nm后很难(摩尔定律还可维持10年)

    集微网消息,台积电董事长张忠谋表示,摩尔定律可能还可再延续10年,3nm制程应该会出来,2nm则有不确定性,2nm之后就很难了. 张忠谋表示,1998年英特尔总裁贝瑞特来台时,两人曾针对摩尔定律还可延 ...

  4. wpf CefSharp 与 js交互

    原文:wpf CefSharp 与 js交互 通过 NuGet 获取 CefSharp.WpF 组件.  xmlns:cefSharp="clr-namespace:CefSharp.Wpf ...

  5. Hadoop MapReduce编程入门案例

    Hadoop入门例程简介 一个.有些指令 (1)Hadoop新与旧API差异 新API倾向于使用虚拟课堂(象类),而不是接口.由于这更easy扩展. 比如,能够无需改动类的实现而在虚类中加入一个方法( ...

  6. 【转载】如何使用docker部署c/c++程序

    原文地址:https://blog.csdn.net/len_yue_mo_fu/article/details/80189035 Docker介绍 Docker是一个开源的容器引擎,它有助于更快地交 ...

  7. MVC 用基架创建Controller,通过数据库初始化器生成并播种数据库

    1 创建MVC应用程序 2 在Model里面创建实体类 using System; using System.Collections.Generic; using System.Linq; using ...

  8. c# 全局钩子实现扫码枪获取信息。

    原文:c# 全局钩子实现扫码枪获取信息. 1.扫描枪获取数据原理基本相当于键盘数据,获取扫描枪扫描出来的数据,一般分为两种实现方式. a)文本框输入获取焦点,扫描后自动显示在文本框内. b)使用键盘钩 ...

  9. 【全面解禁!真正的Expression Blend实战开发技巧】第三章 从最常用ButtonStyle开始 - TextButton

    原文:[全面解禁!真正的Expression Blend实战开发技巧]第三章 从最常用ButtonStyle开始 - TextButton 在实际项目中,使用blend做的最多的一定是各种自定义But ...

  10. SignalR的简单使用(二)

    原文:SignalR的简单使用(二) 之前提到SignalR代理在网页,通过生成的Js来完成相关的功能.但我不禁想一个问题, 难到SignalR的服务端就能寄存在web端吗,通过访问网页能方式才能启动 ...