Redis深度历险-Redis字典源码内部结构

2023-11-02 19:59

本文主要是介绍Redis深度历险-Redis字典源码内部结构,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!

Redis字典的用途

Redis中 hash结构的数据会使用到字典,整个Redis数据库中所有的key和value也组成了一个全局字典,带过期时间的key集合也是一个字典。zset集合中存储value和score值的映射关系也是通过dict结构实现的

struct RedisDb {dict* dict; // all keys key=>valuedict* expires; // all expired keys key=>long(timestamp)...
}struct zset {dict *dict; //all values  value => scorezskiplist *zsl;
}

dict内部结构

dict内部有两个hashtable,通常情况下只有一个hashtable是有值的;在dict扩容或者缩容时需要分配新的hashtable,然后进行渐进式rehash,两个hashtable种存储的分别是新值和旧值;在rehash完成之后,旧的hashtable被删除,新的hashtable会取代旧的hashtable

struct dict {dictType *type;    //类型特定函数void *privdata;    //私有数据dictht ht[2];    //2个哈希表,哈希表负载过高进行rehash的时候才会用到第2个哈希表int rehashidx;    //rehash目前进度,当哈希表进行rehash的时候用到,其他情况下为-1
}
struct dictEntry {void *key;union {void *val;uint64_t u64;    //uint64_t整数int64_t s64;    //int64_t整数}v;struct dictEntry *next;    //指向下个哈希表节点
}
struct dictht {dictEntry **table;    //哈希表数组unsigned long size;    //哈希表大小,即哈希表数组大小unsigned long sizemask; //哈希表大小掩码,总是等于size-1,主要用于计算索引unsigned long used;    //已使用节点数,即已使用键值对数
}

Redis中的hashtable结构和Java的HashMap几乎是一样的,都是通过分桶的方式解决hash冲突。第一维是数组,第二维是链表;数组中存储的是链表的第一个元素的指针

渐进式rehash

大字典扩容是比较耗时的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个O(n)级别的操作,作为单线程的Redis无法接受这样的阻塞;Redis采用渐进式rehash

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
{long index;dictEntry *entry;dictht *ht;// 这里进行小步搬迁if (dictIsRehashing(d)) {_dictRehashStep(d);}/* Get the index of the new element, or -1 if* the element already exists.*/if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) {return NULL;}/* Allocate the memory and store the new entry.* Insert the element in top, with the assumption that in a database* system it is more likely that recently added entries are accessed* more frequently.*///如果字典处于搬迁过程中,要将新的元素挂接到新的数组下面ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];entry = zmalloc(sizeof(*entry));entry->next = ht->table[index];ht->table[index] = entry;ht->used++;/* Set the hash entry fields.*/dictSetKey(d, entry, key);return entry;
}

在客户端对dict进行(hset/hdel等指令时)会触发rehash,除了指令触发rehash,Redis还会在定时任务中对字典进行主动搬迁

// 服务器定时任务
void databaseCron() {
...if (server.activerehashing) {for (j = 0; j < dbs_per_call; j++) {int work_done = incrementallyRehash(rehash_db);if (work_done) {/* If the function did some work, stop here, we'll do* more at the next cron loop.*/break;} else {/* If this db didn't need rehash, we'll try the next one.*/rehash_db++;rehash_db %= server.dbnum;}}}
}

dict查找过程

插入和删除元素都依赖于查找,hashtable的元素是存储在链表中的,所以得先计算出key对应的数组下标;hash_func会将keyhash得出一个整数,不同的key会被映射成分布比较均匀散乱的整数。只有hash均匀之后整个hashtable才是平衡的,二维链表的长度就不会差距很远,查找算法的性能也会比较稳定

func get(key) {let index = hash_func(key) % size;let entry = table[index];while(entry != NULL) {if entry.key == target {return entry.value;}entry = entry.next;}
}

hash函数

Redis字典默认的hash函数是siphash,siphash算法即使在输入key很小的情况下,也可以产生随机性特别好的输出,而且它的性能也非常突出。对于Redis这样的单线程来说,字典数据结构如此普遍,字典操作也会非常频繁,hash函数自然也是越快越好

hash攻击

如果 hash 函数存在偏向性,黑客就可能利用这种偏向性对服务器进行攻击。存在偏向性的 hash 函数在特定模式下的输入会导致 hash 第二维链表长度极为不均匀,甚至所有的元素都集中到个别链表中,直接导致查找效率急剧下降,从O(1)退化到O(n)。有限的服务器计算能力将会被 hashtable 的查找效率彻底拖垮。这就是所谓 hash 攻击。

扩容条件

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d) {/* Incremental rehashing already in progress. Return. */if (dictIsRehashing(d)) {return DICT_OK;}/* If the hash table is empty expand it to the initial size. */if (d->ht[0].size == 0) {return dictExpand(d, DICT_HT_INITIAL_SIZE);}/* If we reached the 1:1 ratio, and we are allowed to resize the hash* table (global setting) or we should avoid it but the ratio between* elements/buckets is over the "safe" threshold, we resize doubling* the number of buckets. */if (d->ht[0].used >= d->ht[0].size &&(dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) {return dictExpand(d, d->ht[0].used*2);}return DICT_OK;
}

正常情况下,当hash表中元素的个数等于一维数组的长度时会开始扩容,扩容的新数组大小是原数组的2倍;若Redis正在做bgsave,为了减少内存页的过多分离(Copy On Write),Redis尽量不去扩容(dict_can_resize);如果hash表已经非常满了,元素个数已经达到了一维数组长度的5倍(dict_force_resize_ratio),说明hash表已经过于拥挤了,这个时候就会强制扩容

缩容条件

int htNeedsResize(dict *dict) {long long size, used;size = dictSlots(dict);used = dictSize(dict);return (size > DICT_HT_INITIAL_SIZE && (used*100/size < HASHTABLE_MIN_FILL));
}

当hash表因为元素的逐渐删减变得越来越稀疏时,Redis会对hash表进行缩容来减少hash表的一维数组空间占用;缩容的条件是元素个数低于数组长度的10%,缩绒不会考虑Redis是否正在做bgsave

set的结构

Redis中的set底层结构也是字典,只不过所有的value都是NULL,其它特性和字典一模一样

为什么缩容不用考虑bgsave

扩容时考虑bgsave是因为,扩容需要申请额外的很多内存,且会重新链接链表(如果会冲突的话), 这样会造成很多内存碎片,也会占用更多的内存,造成系统的压力;而缩容过程中,由于申请的内存比较小,同时会释放掉一些已经使用的内存,不会增大系统的压力,因此不用考虑是否在进行bgsave操作

这篇关于Redis深度历险-Redis字典源码内部结构的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/332957

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达+深度摄像机的仿真小车,并使用通过跨平台的方式进行ROS2和UE5仿真的通讯,达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础,Nav2相关的学习教程可以参考本人的其他博客Nav2代价地图实现和原理–Nav2源码解读之CostMap2D(上)-CSDN博客往期教程: 第一期:基于UE5和ROS2的激光雷达+深度RG

韦季李输入法_输入法和鼠标的深度融合

在数字化输入的新纪元,传统键盘输入方式正悄然进化。以往,面对实体键盘,我们常需目光游离于屏幕与键盘之间,以确认指尖下的精准位置。而屏幕键盘虽直观可见,却常因占据屏幕空间,迫使我们在操作与视野间做出妥协,频繁调整布局以兼顾输入与界面浏览。 幸而,韦季李输入法的横空出世,彻底颠覆了这一现状。它不仅对输入界面进行了革命性的重构,更巧妙地将鼠标这一传统外设融入其中,开创了一种前所未有的交互体验。 想象

POJ2001字典树

给出n个单词,求出每个单词的非公共前缀,如果没有,则输出自己。 import java.io.BufferedReader;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWriter;import java.io.UnsupportedEncodingException;

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

免费也能高质量!2024年免费录屏软件深度对比评测

我公司因为客户覆盖面广的原因经常会开远程会议,有时候说的内容比较广需要引用多份的数据,我记录起来有一定难度,所以一般都用录屏工具来记录会议内容。这次我们来一起探索有什么免费录屏工具可以提高我们的工作效率吧。 1.福晰录屏大师 链接直达:https://www.foxitsoftware.cn/REC/  录屏软件录屏功能就是本职,这款录屏工具在录屏模式上提供了多种选项,可以选择屏幕录制、窗口