IO模型

  • 概述
  • 实现
  • 字典
  • 哈希表
  • 哈希表dictht结构
  • 哈希表节点dictEntry结构
  • 哈希算法
  • 解决键冲突
  • rehash
  • 渐进式rehash
  • API
  • 参考文献


概述

字典又称映射(map), 是一种用于保存键值对的数据结构。

字典的键(key)唯一一个键对应一个值(value),查找,删除,更新都需要通过键来操作。

redis的数据库就是使用字典作为底层

新增键值对"msg"->"hello world", 执行redis命令:
redis> SET msg "hello world"

字典也是哈希键的底层实现之一

实现

字典

字典结构 dict.h/dict

typedef struct dict{
	// 类型特定函数
	dictType *type;
	// 私有数据
	void *privdata;
	// 哈希表
	dictht ht[2];
	// rehash索引 当rehash不在进行时值为-1
	int trehashidx;
}dict;
  • type 指向dictType结构体的指针, 该结构体保存了对特性类型键值对操作的函数
  • privadata 保存type中函数的特定参数.
  • ht 包含两个dictht哈希表, 一般只是使用ht[0], ht[1]在rehash时才会使用
  • rehashidx 记录了rehash目前的进度.

哈希表

哈希表dictht结构

typedef struct dictht{
	// 哈希表数组
	dictEntry **table;
    // 哈希表大小
    unsigned long size;
    //值为 size - 1 掩码 计算索引
    unsigned long sizemask;
    //
    unsigned long used;
}dictht;
  • table 中的元素都是dict.h/dictEntry结构, 是一个保存着键值对的哈希表节点.
  • size 为哈希表的大小, 即table数组大小.
  • sizemask 值等于size - 1, 用于计算索引.
  • used 记录哈希表已有节点

哈希表节点dictEntry结构

dictht中table数组保存的键值对元素.

typedef struct dictEntry{
	// 键
	void *key;
	// 值
	union{
		void *val;
		uint64_t u64;
		int64_t s64;
	}v;
	
	// 指向下个哈希表的节点, 形成链表.
	struct dictEntry *next;
}dictEntry;
  • key 保存着键值对中的键
  • v 是一个共用体, 表示键值对中的值, 它可以是void * 指针, uint64_t无符号整数和 int64_t整数其中的一个.
  • next 指向下一个节点的指针, 链地址法解决哈希冲突.

普通状态下的字典结构

redis存放一个Map对象 redis存储map的结构_键值对

哈希算法

  1. 使用字段设置的特定函数中的哈希函数计算哈希值
hash = dict->type->hashFunction(key);
  1. 通过sizemask属性和哈希值, 计算出索引值
index = hash & dict->ht[x].sizemask;

解决键冲突

  • Redis 的哈希表采用链地址法解决哈希冲突
  • 多个相同hash值的哈希表节点使用next指针构成一个单向链表.

rehash

  • 让哈希表的负载因子维持在一个合理的范围, 应对哈希表底层的数组进行相应的扩增或缩小

rehash步骤:

  • 为字典的ht[1]哈希表分配空间, 扩展则其大小为于 第一个大于等于ht[0].used * 2 的 2^n。 收缩则其大小为第一个小于等于 ht[0].used 的2^n。
  • 将ht[0]中的所有键值对rehash到ht[1] 中(经过重新计算)。
  • 释放ht[0], 将ht[1] 设为 ht[0]。

rehash条件:

  • 服务器目前没有执行BGSAVE命令和BGREWRITEAOF命令, 且哈希表负载因子大于1
  • 服务器正在执行BGSAVE命令和BGREWRITEAOF命令, 且哈希表负载因子大于5
  • 哈希表负载因子小于0.1, 则会进行哈希表的收缩
  • 负载因子 = 哈希表已保存节点数量 / 哈希表大小

BGSAVE和BGREWRITEAOF命令会创建服务进程的子进程,扩展操作会影响写时复制,造成内存浪费。

写时复制:对于父进程和子进程,其共用一份内存空间,只有进程空间的各段的内容要发生变化时,才会将父进程的内存复制一份给子进程。

渐进式rehash

rehash存在的问题:

  • 存有大量数据时扩展较慢,影响正常地增删改查操作

解决:

  • 使用渐进式rehash,将rehash的工作均摊每一次的增删改查上。

渐进式rehash步骤:

  1. 为ht[1] 分配空间, 让字典同时持有ht[0] 和 ht[1]两个哈希表
  2. 维持一个索引计数器变量rehashidx = 0
  3. 每次对字典的增删改查操作, 都会顺带将 ht[0] 中 rehashdx 位置的所有键值对rehash到 ht[1] 上. rehashidx向后递增。
  4. 随着对字典操作的进行,所有的ht[0] 中的键值对都会被rehash到 ht[1],最后设置rehashidx = -1, 渐进式 rehash 完成。

渐进式rehash时对字典进行操作:

  • 所有的增删改查都会根据情况同时再ht[0] 和 ht[1] 上同时操作。
  • 查找操作,会先在ht[0]中查找, 不存在则再在ht[1]中查找
  • 新增操作, 都会在ht[1]上进行。

API

函数

作用

时间复杂度

dictCreate

创建一个新字典

O(1)

dictAdd

添加键值对

O(1)

dictReplace

替换键值对, 不存在则添加

O(1)

dictFetchValue

返回给定键的值

O(1)

dictGetRandomKey

随机返回一个键值对

O(1)

dictDelete

删除键值对

O(1)

dictRelease

释放字典, 以及所有键值对

O(N)