Redis的设计与实现(3)-字典
Redis 的数据库使用字典实现, 对数据库的增, 删, 查, 改也是构建在对字典的操作之上的.
字典是哈希键的底层实现之一: 当一个哈希键包含的键值对比较多, 又或者键值对中的元素都是比较长的字符串时, Redis 将会使用字典作为哈希键的底层实现.
1. 哈希表
Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有多个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对.
Redis 字典所使用的哈希表由 dict.h/dictht 结构定义:
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
table 属性是一个数组, 数组中的每个元素都是一个指向 dict.h/dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对.
size 属性记录了哈希表的大小, 也即是 table 数组的大小, 而 used 属性则记录了哈希表目前已有节点(键值对)的数量.
sizemask 属性的值总是等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面.
2. 哈希表节点
哈希表节点使用 dictEntry 结构表示, 每个 dictEntry 结构都保存着一个键值对:
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
- key 键
- v 值
- next 指向下一个哈希表节点指针
- 值可以是一个
指针,uint64_t 整数,int64_t 整数; next可以将多个哈希值相同的键值对连接在一次, 以此解决键冲突的问题.
3.字典
Redis 中的字典由 dict.h/dict 结构表示:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:
- type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数;
- privdata 属性则保存了需要传给那些类型特定函数的可选参数.
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;
ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用.
除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash, 那么它的值为 -1.
4. 哈希算法
当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出哈希值和索引值, 然后再根据索引值, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上
面.
Redis 计算哈希值和索引值的方法如下:
# 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
# 使用哈希表的 sizemask 属性和哈希值,计算出索引值
# 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;
当字典被用作数据库或哈希键的底层实现时, Redis 使用 MurmurHash2 算法来计算键的哈希值.
MurmurHash 算法目前的最新版本为 MurmurHash3 , 而 Redis 使用的是 MurmurHash2, 关于 MurmurHash 算法的更多信息可以参考该算法的主页: http://code.google.com/p/smhasher/.
该算法已经迁移到 GitHub: https://github.com/aappleby/smhasher
然而, 我在 GitHub 上搜索 Redis 的相关源码, 发现版本不同, 使用的哈希算法也是不相同的:
最新版本的 Redis (5.0已发布), 采用的是 SipHash 哈希算法, 链接:
https://github.com/antirez/redis/blob/unstable/src/dict.c#L84
往回看, 5.0, 4.0 都是 SipHash 哈希算法:
https://github.com/antirez/redis/blob/5.0/src/dict.c#L84
https://github.com/antirez/redis/blob/4.0/src/dict.c#L84
3.2, 3.0, 2.8, 2.6 都是 MurmurHash2 哈希算法:
https://github.com/antirez/redis/blob/3.2/src/dict.c#L92
https://github.com/antirez/redis/blob/3.0/src/dict.c#L92
https://github.com/antirez/redis/blob/2.8/src/dict.c#L92
https://github.com/antirez/redis/blob/2.6/src/dict.c#L98
而更旧的 2.4 2.2, 作者没有说是什么算法, 但是让我回想到 鸟哥 的一篇文章, 讲的是 PHP 的哈希算法, 看样子很像, 估计是 DJBX33A (Daniel J. Bernstein, Times 33 with Addition) 算法.
https://github.com/antirez/redis/blob/2.4/src/dict.c#L88
https://github.com/antirez/redis/blob/2.2/src/dict.c#L88
关于哈希算法的, 还找到了两篇文章:
5. 解决键冲突
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision).
Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 next 指针, 多个哈希表节点可以用 next 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题.
因为 dictEntry 节点组成的链表没有指向链表表尾的指针, 为了速度考虑, 程序总是将新节点添加到链表的表头位置, 排在已有节点前面, 操作的复杂度为 O(1).
6. rehash
当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩.
扩展和收缩哈希表的工作可以通过执行 rehash (重新散列) 操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下:
- 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
- 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
- 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n .
- 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上.
- 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备.
7. 哈希表的扩展与收缩
当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1;
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5. (感觉书错了, 应该是 0.5 ...)
其中哈希表的负载因子可以通过公式计算得出:
# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制 (copy-on-write) 技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载
因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存.
另一方面, 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作.
8. 渐进式 rehash
为避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0] 里面的所有键值对全部 rehash 到 ht[1] , 而是分多次, 渐进式地将 ht[0] 里面的键值对慢慢地 rehash 到 ht[1].
以下是哈希表渐进式 rehash 的详细步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表;
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始;
- 在 rehash 进行期间, 每次对字典执行添加, 删除, 查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增 1;
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成.
渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加, 删除, 查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量.
9. 字典 API
| 函数 | 作用 | 时间复杂度 |
|---|---|---|
| dictCreate | 创建一个新的字典. | O(1) |
| dictAdd | 将给定的键值对添加到字典里面. | O(1) |
| dictReplace | 将给定的键值对添加到字典里面, 如果键已经存在于字典,那么用新值取代原有的值. | O(1) |
| dictFetchValue | 返回给定键的值. | O(1) |
| dictGetRandomKey | 从字典中随机返回一个键值对. | O(1) |
| dictDelete | 从字典中删除给定键所对应的键值对. | O(1) |
| dictRelease | 释放给定字典,以及字典中包含的所有键值对. | O(N),N为字典包含的键值对数量. |
10. 总结
- 字典被广泛用于实现 Redis 的各种功能, 其中包括数据库和哈希键;
- Redis 中的字典使用哈希表作为底层实现, 每个字典带有两个哈希表, 一个用于平时使用, 另一个仅在进行 rehash 时使用.
当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用 MurmurHash2 算法来计算键的哈希值; - 哈希表使用链地址法来解决键冲突, 被分配到同一个索引上的多个键值对会连接成一个单向链表;
- 在对哈希表进行扩展或者收缩操作时, 程序需要将现有哈希表包含的所有键值对 rehash 到新哈希表里面, 并且这个 rehash 过程并不是一次性地完成的, 而是渐进式地完成的.
文章来源于本人博客,发布于 2018-05-12,原文链接:https://imlht.com/archives/125/
Redis的设计与实现(3)-字典的更多相关文章
- [Redis]Redis的设计与实现-链表/字典/跳跃表
redis的设计与实现:1.假如有一个用户关系模块,要实现一个共同关注功能,计算出两个用户关注了哪些相同的用户,本质上是计算两个用户关注集合的交集,如果使用关系数据库,需要对两个数据表执行join操作 ...
- Redis的设计与实现——字典
参考博客 绝大多数语言中的字典底层实现基本上都是哈希表.哈希表中用 “负载因子” 来衡量哈希表的 空/满 程度.为了让负载因子在一定的合理范围之内,提高查询的性能,一般的做法是让哈希表扩容,然后reh ...
- Redis --> Redis架构设计
Redis架构设计 一.前言 Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 字符串(strings), 散列 ...
- 11.Redis缓存设计
11.Redis缓存设计11.1 缓存的收益和成本11.2 缓存更新策略11.3 缓存粒度控制11.4 穿透优化11.5 无底洞优化11.6 雪崩优化11.7 热点key重建优化11.8 本章重点回顾
- Redis缓存设计及常见问题
Redis缓存设计及常见问题 缓存能够有效地加速应用的读写速度,同时也可以降低后端负载,对日常应用的开发至关重要.下面会介绍缓存使 用技巧和设计方案,包含如下内容:缓存的收益和成本分析.缓存更新策略的 ...
- Python 基于python+mysql浅谈redis缓存设计与数据库关联数据处理
基于python+mysql浅谈redis缓存设计与数据库关联数据处理 by:授客 QQ:1033553122 测试环境 redis-3.0.7 CentOS 6.5-x86_64 python 3 ...
- redis数据库设计(转)
原文:http://segmentfault.com/q/1010000000316112 redis是什么 redis就是一个存储key-value键值对的仓库,如何使用redis在于如何理解你需要 ...
- 17 redis -key设计原则
书签系统 create table book ( bookid int, title char(20) )engine myisam charset utf8; insert into book va ...
- 细说分布式Redis架构设计和踩过的那些坑
细说分布式Redis架构设计和踩过的那些坑_redis 分布式_ redis 分布式锁_分布式缓存redis 细说分布式Redis架构设计和踩过的那些坑
- Redis源码解析:03字典
字典是一种用于保存键值对(key value pair)的抽象数据结构.在字典中,一个键和一个值进行关联,就是所谓的键值对.字典中的每个键都是独一无二的,可以根据键查找.更新值,或者删除整个键值对等等 ...
随机推荐
- AI测试101:测试AI系统的实用技巧&ML和AI自动化工具
基于人工智能的系统,也称为神经网络(NN Neural Networks),和其他应用程序一样是 "系统",因此需要测试.本文将指导你测试AI和基于NN的系统,并理解相关概念. 测 ...
- vue2中使用composition-api
vue2中使用composition-api https://juejin.cn/post/6874927606820274184 vue3.0 watch 函数 https://www.jiansh ...
- js中数组的sort() 方法
sort() 方法用于对数组的元素进行排序,并返回数组.默认排序顺序是根据字符串UniCode码.因为排序是按照字符串UniCode码的顺序进行排序的,所以首先应该把数组元素都转化成字符串(如有必要 ...
- C# 几种获取电脑内存、CPU信息的方案
计数器.WMI 获取设备的内存信息,如系统可用运行内存: 1 public static async Task<double> GetMemoryAvailableAsync(FileSi ...
- [OpenCV-Python] 8 用滑动条做调色板
文章目录 OpenCV-Python:II OpenCV 中的 Gui 特性 8 用滑动条做调色板 8.1 代码示例 练习 OpenCV-Python:II OpenCV 中的 Gui 特性 8 用滑 ...
- .NET开源分布式锁DistributedLock
一.线程锁和分布式锁 线程锁通常在单个进程中使用,以防止多个线程同时访问共享资源. 在我们.NET中常见的线程锁有: 自旋锁:当线程尝试获取锁时,它会重复执行一些简单的指令,直到锁可用 互斥锁: Mu ...
- Locust 运行方式
命令参数方式运行 # -*- coding: utf-8 -*- from locust import TaskSet, task, User ''' 命令行参数运行示例代码 ''' class ...
- 麻了,一个操作把MySQL主从复制整崩了
前言 最近公司某项目上反馈mysql主从复制失败,被运维部门记了一次大过,影响到了项目的验收推进,那么究竟是什么原因导致的呢?而主从复制的原理又是什么呢?本文就对排查分析的过程做一个记录. 主从复制原 ...
- 2023-04-08:社交网络中的最优邀请策略探究。本文以小红准备开宴会为例,提出一种基于贪心算法和二分查找的解决方案,帮助读者在保证愉悦值不低于k的前提下,最小化宴会的阶层差距。
2023-04-08:小红有n个朋友, 她准备开个宴会,邀请一些朋友, i号朋友的愉悦值为a[i],财富值为b[i], 如果两个朋友同时参加宴会,这两个朋友之间的隔阂是其财富值差值的绝对值, 宴会的隔 ...
- 2022-10-16:以下go语言代码输出什么?A:timed out;B:panic;C:没有任何输出。 package main import ( “context“ “fmt“
2022-10-16:以下go语言代码输出什么?A:timed out:B:panic:C:没有任何输出. package main import ( "context" &quo ...