Redis存储原理
前言
我们从redis服务谈起,redis是单reactor,命令在redis-server线程处理。还有若干读写IO线程负责IO操作(redis6.0之后,Redis之pipeline与事务)。此外还有一个内存池线程负责内存管理、一个后台文件线程负责大文件的关闭、一个异步刷盘线程负责持久化。这就使得redis在设计方面对高效的贡献。

从io密集型的角度来看,redis的磁盘IO是fork子进程异步刷盘,不占用主线程。网络IO如果有多个连接,并且发送了大量的请求,才会考虑IO多线程;从cpu密集型的角度来看,redis有高效的数据结构,加之kv原理,避免了cpu密集。那redis为什么不采用多线程呢?硬伤就是redis的多种数据类型由多种不同的数据结构实现,加锁复杂,锁的粒度不易控制。此外频繁的上下文切换会降低整体性能。那除了单reactor,redis还在什么方面对高效做出来贡献呢?hash表。
存储原理
redis使用哈希表来组织所有数据,这次我们直接杀到源码。在redisDb中我们可以看到四个dict(散列表)类型的成员(英文这里就不翻译了,原汁原味)。
typedef struct redisDb {kvstore *keys; /* The keyspace for this DB */kvstore *expires; /* Timeout of keys with a timeout set */ebuckets hexpires; /* Hash expiration DS. Single TTL per hash (of next min field to expire) */dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/dict *blocking_keys_unblock_on_nokey; /* Keys with clients waiting for* data, and should be unblocked if key is deleted (XREADEDGROUP).* This is a subset of blocking_keys*/dict *ready_keys; /* Blocked keys that received a PUSH */dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */int id; /* Database ID */long long avg_ttl; /* Average TTL, just for stats */unsigned long expires_cursor; /* Cursor of the active expire cycle. */list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
可见我们需要对dict下手了,因为数据就存储在dict中。这其实类似c++类的封装,dictType中存放了所有成员函数,其中dictEntry指针数组就是元素存储的地方了。
struct dict {dictType *type;dictEntry **ht_table[2];//哈希表unsigned long ht_used[2];//实际存储的元素个数long rehashidx; /* rehashing not in progress if rehashidx == -1 *//* Keep small vars at end for optimal (minimal) struct padding */unsigned pauserehash : 15; /* If >0 rehashing is paused */unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above */signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) */void *metadata[];
};
我们来看dictEntry,也就是存储元素的结构。
/* -------------------------- types ----------------------------------------- */
struct dictEntry {void *key;union {void *val;uint64_t u64;int64_t s64;double d;} v;struct dictEntry *next; /* Next entry in the same hash bucket. */
};typedef struct {void *key;dictEntry *next;
} dictEntryNoValue;
源码我们找到了,现在就可以来讨论一下哈希表了。哈希表通过hash运算得出index,如果这个地方有元素,就通过链表组织。这就对应了dictEntry **ht_table[2]。

我们知道,通过链表组织就是一种应对hash冲突的策略,hash冲突会使索引效率降低,然鹅从源码可以看到有一个负载因子,也就是已有元素与数组长度之比(used/size),负载因子越大表示hash冲突就越大,而我们知道hash时间复杂度是O(1),hash冲突到后面会使时间复杂度增加到O(n)。当负载因子大于1的时候就要进行扩容操作,也就是将数组长度翻倍,增大分母减小负载因子。而当负载因子小于0.1(恰好大于used的2^n)的时候就要进行缩容,因为这是占着茅坑不拉屎,浪费空间。不管扩容还是缩容,数组长度都会改变,这时候hash算法就变了,就要对原来的元素重新hash。

蛋是!redis是个数据库啊,rehash时就有可能有数据操作,所以它就不能像STL中的unordered_map一样直接rehash(CPU密集型的)。redis是采取渐进式rehash策略。Redis的渐进式rehash是一种在数据量增加时对哈希表进行动态扩容的机制,它能够保证数据的均匀分布和快速访问,同时避免了传统rehash操作可能带来的性能下降问题。在Redis中,渐进式rehash的实现原理和步骤如下:
- 触发条件:当哈希表中的节点数与哈希表大小的比例超过一定阈值(默认为1:1,或者在执行RDB或AOF操作时为5:1)时,就会触发rehash操作。
- 创建新的哈希表:Redis会创建一个新的哈希表,其大小是原哈希表的两倍,以容纳更多的键值对。
- 渐进式迁移:Redis不会一次性将所有键值对迁移到新哈希表,而是将迁移过程分散到后续的字典操作中。每次字典操作时,会顺便迁移一部分键值对到新哈希表,这个过程称为渐进式rehash。
- 迁移过程:在迁移过程中,Redis会维护一个
rehashidx索引,用于记录迁移的进度。每次字典操作时,会迁移rehashidx索引对应的桶中的所有键值对到新哈希表,然后rehashidx递增。 - 读写操作:在渐进式rehash进行期间,查找操作会在两个哈希表中进行,以确保不会遗漏任何键值对。而新增操作则只会在新的哈希表中进行。
- 完成rehash:当所有键值对都迁移到新哈希表后,Redis会将旧哈希表的空间释放,并将
rehashidx重置为-1,表示rehash操作完成。 - 缩容操作:除了扩容,Redis在数据量减少时也会进行缩容操作,其过程与扩容类似,也是通过渐进式rehash来完成。
渐进式rehash的优点在于它能够平滑地处理数据迁移,避免了一次性迁移可能导致的性能抖动,确保了Redis在扩容或缩容过程中的稳定性和性能。然而,它也会占用额外的内存空间,因为需要同时维护两个哈希表,并且在迁移过程中可能会导致一定程度的数据不一致。尽管如此,渐进式rehash是Redis中一个非常重要的特性,它使得Redis能够有效地处理动态数据集的变化。rehash源码如下,这里就不逐行解释了,扔给AI便可。
int dictRehash(dict *d, int n) {int empty_visits = n*10; /* Max number of empty buckets to visit. */unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;/* If dict_can_resize is DICT_RESIZE_AVOID, we want to avoid rehashing. * - If expanding, the threshold is dict_force_resize_ratio which is 4.* - If shrinking, the threshold is 1 / (HASHTABLE_MIN_FILL * dict_force_resize_ratio) which is 1/32. */if (dict_can_resize == DICT_RESIZE_AVOID && ((s1 > s0 && s1 < dict_force_resize_ratio * s0) ||(s1 < s0 && s0 < HASHTABLE_MIN_FILL * dict_force_resize_ratio * s1))){return 0;}while(n-- && d->ht_used[0] != 0) {/* Note that rehashidx can't overflow as we are sure there are more* elements because ht[0].used != 0 */assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);while(d->ht_table[0][d->rehashidx] == NULL) {//旧数据为空就找下一个d->rehashidx++;if (--empty_visits == 0) return 1;}/* Move all the keys in this bucket from the old to the new hash HT */rehashEntriesInBucketAtIndex(d, d->rehashidx);d->rehashidx++;}return !dictCheckRehashingCompleted(d);
}static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) {dictEntry *de = d->ht_table[0][idx];uint64_t h;dictEntry *nextde;while (de) {nextde = dictGetNext(de);void *key = dictGetKey(de);/* Get the index in the new hash table */if (d->ht_size_exp[1] > d->ht_size_exp[0]) {h = dictHashKey(d, key, 1) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);} else {/* We're shrinking the table. The tables sizes are powers of* two, so we simply mask the bucket index in the larger table* to get the bucket index in the smaller table. */h = idx & DICTHT_SIZE_MASK(d->ht_size_exp[1]);}if (d->type->no_value) {if (d->type->keys_are_odd && !d->ht_table[1][h]) {/* Destination bucket is empty and we can store the key* directly without an allocated entry. Free the old entry* if it's an allocated entry.** TODO: Add a flag 'keys_are_even' and if set, we can use* this optimization for these dicts too. We can set the LSB* bit when stored as a dict entry and clear it again when* we need the key back. */assert(entryIsKey(key));if (!entryIsKey(de)) zfree(decodeMaskedPtr(de));de = key;} else if (entryIsKey(de)) {/* We don't have an allocated entry but we need one. */de = createEntryNoValue(key, d->ht_table[1][h]);} else {/* Just move the existing entry to the destination table and* update the 'next' field. */assert(entryIsNoValue(de));dictSetNext(de, d->ht_table[1][h]);}} else {dictSetNext(de, d->ht_table[1][h]);}d->ht_table[1][h] = de;d->ht_used[0]--;d->ht_used[1]++;de = nextde;}d->ht_table[0][idx] = NULL;
}
那我们怎么访问元素?用keys命令吗?非也。当执行 KEYS 命令时,Redis 会锁定数据库,然后检索所有匹配给定模式的键。如果键的数量非常多,这个操作可能会花费很长时间,在此期间,服务器不能响应其他客户端的请求,从而导致服务器阻塞。这是因为 KEYS 命令在执行期间会阻止其他操作,包括但不限于写操作,直到命令执行完成。我们可以使用scan命令。Redis 的 SCAN 命令是一个基于游标的迭代器,用于逐个元素地访问(或迭代)集合类型的元素,如列表(list)、集合(set)、有序集合(sorted set)和哈希(hash)。这个命令非常有用,因为它提供了一种方式来逐步检索大数据集,而不会像使用传统的 KEYS 命令那样对服务器性能造成严重影响。SCAN 命令的基本用法如下:
SCAN cursor [MATCH pattern] [COUNT count]
cursor:游标,每次调用SCAN命令时,都会返回一个新的游标,用于下一次迭代。MATCH pattern:可选参数,用于过滤返回的元素,只返回符合给定模式的元素。COUNT count:可选参数,建议 Redis 返回的元素数量,实际返回的数量可能会更多或更少。
SCAN 命令的优点包括:
- 性能:相比于
KEYS命令,SCAN命令不会阻塞服务器,因为它是迭代地返回元素,而不是一次性返回所有匹配的元素。 - 大数据集:对于包含大量元素的集合类型,
SCAN命令可以有效地进行分批处理,这对于处理大数据集非常有用。 - 模式匹配:通过
MATCH选项,可以只返回符合特定模式的元素,这在处理具有特定前缀或模式的键时非常有用。 - 分页:在分页应用中,
SCAN可以用于实现服务器端的分页功能,通过游标来控制返回的数据范围。 - 灵活性:
COUNT参数允许你建议 Redis 返回的元素数量,这有助于控制每次迭代的负载。
需要注意的是,SCAN 命令返回的元素可能会有重复,因为 Redis 并不保证每次迭代都是从上一次停止的地方开始。因此,客户端需要准备好处理可能的重复数据。此外,SCAN 命令提供的迭代器只能保证在迭代过程中添加的新元素会被返回,但不保证它们的顺序。
相关文章:
Redis存储原理
前言 我们从redis服务谈起,redis是单reactor,命令在redis-server线程处理。还有若干读写IO线程负责IO操作(redis6.0之后,Redis之pipeline与事务)。此外还有一个内存池线程负责内存管理、一个后台文件线程负责大文件的关…...
PHP、Java等其他语言转Go时选择GoFly快速快速开发框架指南
概要 经过一年多的发展GoFly快速开发框架已被一千多家科技企业或开发者用于项目开发,它的简单易学得到其他语言转Go首选框架。且企业版的发展为GoFly社区提供资金,这使得GoFly快速框架得到良好的发展,GoFly技术团队加大投入反哺科技企业和开…...
【MySQL】获取最近7天和最近14天的订单数量,使用MySQL详细写出,使用不同的方法
1. 获取最近7天和最近14天的订单数量,使用MySQL详细写出,使用不同的方法 要获取最近7天和最近14天的订单数量,我们可以使用不同的方法来优化查询性能。以下是两种方法: 1.1 方法一:使用日期计算 SELECTSUM(CASE WHE…...
WebView2新增、修改、删除、禁用右键菜单相关操作。
参考链接:WebView2操作右键菜单...
使用vue创建项目
一、安装环境 二、创建vue框架(创建文件夹,摁shift鼠标右键 打开) 1、项目配置 2、新增目录 三、路径别名配置 输入/ ,VSCode会联想出src下的所有子目录和文件,统一文件路径访问时不容易出错 四、ElementPlus配置 1、组件分为…...
Apache CVE-2021-41773 漏洞攻略
漏洞简介 该漏洞是由于Apache HTTP Server 2.4.49版本存在⽬录穿越漏洞,在路径穿越⽬录 <Directory/>Require all granted</Directory>允许被访问的的情况下(默认开启),攻击者可利⽤该路径穿越漏洞读取到Web⽬录之外的其他⽂件在…...
【redis-02】深入理解redis中RBD和AOF的持久化
redis系列整体栏目 内容链接地址【一】redis基本数据类型和使用场景https://zhenghuisheng.blog.csdn.net/article/details/142406325【二】redis的持久化机制和原理https://zhenghuisheng.blog.csdn.net/article/details/142441756 如需转载,请输入:htt…...
亚马逊IP关联揭秘:发生ip关联如何处理
在亚马逊这一全球领先的电商平台上,IP关联是一个不可忽视的问题,尤其是对于多账号运营的卖家而言。本文将深入解析亚马逊IP关联的含义、影响以及应对策略,帮助卖家更好地理解和应对这一问题。 什么是亚马逊IP关联? 亚马逊IP关联…...
jQuery Mobile 弹窗
jQuery Mobile 弹窗 引言 在移动设备上,弹窗是一种常见的用户界面元素,用于显示信息、获取用户输入或提供特定功能。jQuery Mobile 是一个流行的移动框架,它提供了丰富的组件来帮助开发者创建响应式的移动界面。本文将重点介绍如何在 jQuery Mobile 中使用弹窗(Popup)组…...
【macOS】【zsh报错】zsh: command not found: python
【macOS】【zsh Error】zsh: command not found: python 本地已经安装了Python,且能在Pycharm中编译Python程序并运行。 但是,在macOS终端,运行Python,报错。 首先要确认你在macOS系统下,是否安装了Python。 如果安…...
NoSql数据库Redis知识点
数据库的分类 关系型数据库 ,是建立在关系模型基础上的数据库,其借助于集合代数等数学概念和方法来处理数据库 中的数据主流的 MySQL 、 Oracle 、 MS SQL Server 和 DB2 都属于这类传统数据库。 NoSQL 数据库 ,全称为 Not Only SQL &a…...
Redis 使用指南
Redis 使用指南 概述 Redis 是一个开源的、基于内存的数据结构存储系统,可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(strings)、哈希(hashes)、列表(lists…...
c++与cmake:完整的C++项目构建注意事项
个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 最近常常使用cmake构建c项目有感,从创建项目到打包发布总结一下需要注意的事情. 项目组织方式 具体的项目组织方式因人而异,这里推荐一种,在src目录中创建模块目录,再在include目录中常见对应的同名目录包含头文件,…...
Linux自主学习篇
用户及权限管理 sudo 是 "superuser do" 的缩写,是一个在类 Unix 操作系统(如 Linux 和 macOS)中使用的命令。它允许普通用户以超级用户(root 用户)的身份执行命令,从而获得更高的权限。 useradd…...
MQ入门(4)
Erlang:面向高并发的 单机的吞吐量就是并发性:Rabbitmq是10w左右(现实项目中已经足够用了),RocketMQ是10w到20w,Kafka是100w左右。 公司里的并发(QPS) 大部分的公司每天的QPS大概…...
linux下共享内存的3种使用方式
进程是资源封装的单位,内存就是进程所封装的资源的一种。一般情况下,进程间的内存是相互隔离的,也就是说一个进程不能访问另一个进程的内存。如果一个进程想要访问另一个进程的内存,那么必须要进过内核这个桥梁,这就是…...
伊丽莎白·赫莉为杂志拍摄一组素颜写真,庆祝自己荣膺全球最性感女人第一名
语录:女性应该做任何她们想做的事,批评她们的人都见鬼去吧。 伊丽莎白赫莉为《Maxim》杂志拍摄一组素颜写真,庆祝自己荣膺全球最性感女人第一名 伊丽莎白赫莉 (Elizabeth Hurley) 实在是太惊艳了,如今,《马克西姆》杂…...
Qt快捷键说明与用法
编辑与查找 CtrlF:在当前编辑窗口中查找关键字。支持大小写相关、全词匹配、正则表达式匹配等选项,并且查找之后还可以进行替换操作。 CtrlShiftF:进行全局查找,不局限于当前文件。注意,在某些情况下,这个…...
技术周刊 | TS 5.6、Chrome DevTools 性能面板上新、Vite 6 Beta、Fastify v5、HTTP 新方法 Query
增长能力,就是持续做出正确决定的能力。 大家好,我是童欧巴,欢迎来到第 128 期技术周刊。 资讯 TypeScript 5.6 TypeScript 5.6 如期发布。 Chrome DevTools 发布全新性能功能 Chrome DevTools 的性能面板上新测试,包括 Core…...
使用Mockito进行单元测试
1、单元测试介绍 Mockito和Junit是用于单元测试的常用框架。单元测试即:从最小的可测试单元(如函数、方法或类)开始,确保每个单元都能按预期工作。单元测试是白盒测试的核心部分,它有助于发现单元内部的错误。 单元测试…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...
【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...
stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...
Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
