news 2026/2/8 6:05:04

Redis执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis执行

我们之前讲了Redis中数据对象的存储,大家就好奇了,我既然知道这些对象存储的底层原理,那么整体在Redis中是怎么存储的呢?Redis作为内存存储,前面提到过我们放在Redis中的数据都是以键值对形式存储的,本次我们会学习Redis底层结构到底长什么样?

一.内存存储

在Redis中,redisDb代表Redis数据库结构,而我们之前讲的各种操作对象是存储在dict数据结构里的,下面是redisDb的结构代码:

typedef struct redisDb { dict *dict; // 主字典,存储所有键值对 dict *expires; // 过期时间字典,存储key的过期时间 dict *blocking_keys; // 阻塞键字典(BLPOP等命令使用) dict *ready_keys; // 就绪键字典 dict *watched_keys; // 监视键字典(事务WATCH使用) int id; // 数据库ID(0-15) long long avg_ttl; // 平均TTL(用于统计) list *defrag_later; // 稍后碎片整理的键列表 } redisDb;

在该结构中,我们需要重点关注其中两个字段: dict和expires;

2.1dict结构

我们先重点关注一i下dict结构,它代表我们存入的key-value存储,我们平常添加数据,就是往dict里添加。可以看到,dic就是我们前面介绍的Hash对象结构:

typedef struct dict { dictType *type; void *privdata; dicht ht[2]; long rehashidx; /* rehashing */ unsigned long iterators; /* */ } dict;

下面这张图更加生动的说明了redis中底层数据存储是什么样的:redisDb即为数据库对象,指向了数据字典,字典里包含了我们平常存储的k-v数据,k是字符串对象,value支持任意Redis对象,这些对象我们前面有介绍。

当时我学到这的时候,有一个疑问,Redis中redisDb有一个dict字段用于存入key-value;而这个字段里有一个dictht ht[2]代表两个hashtable,其中我们最常用的是h[0],那么我在使用hash对象的时候会使用到hashtable,如果我再加一个hash对象不是又需要用到新的hashtable,可是一个数据库不是只有一个dict,而这个dict只对应唯一dictht ht[2],那怎么办。后面查询资料才知道是我的误解,我混淆了两个不同层次的哈希表:

1. 数据库层面的哈希表(redisDb中的dict)

  • redisDb.dict是用来存储整个数据库所有键值对
  • dict.ht[0]dict.ht[1]是用于渐进式rehash
  • 这里存的是:key(字符串)→ value(可以是string、list、hash、set等任意类型)

2. Hash对象内部的哈希表

  • 当value的类型是hash时,这个hash对象内部有自己独立的dict结构
  • 每个hash对象都有自己的dict,也有自己的ht[0]ht[1]
redisDb └── dict (数据库级别的字典) └── ht[0] / ht[1] (用于渐进式rehash) ├── key: "user:1" → value: Hash对象 │ └── dict (这个hash对象自己的字典) │ └── ht[0] / ht[1] │ ├── "name" → "张三" │ ├── "age" → "25" │ └── "city" → "北京" │ ├── key: "user:2" → value: Hash对象 │ └── dict (另一个独立的字典) │ └── ht[0] / ht[1] │ ├── "name" → "李四" │ └── "age" → "30" │ └── key: "counter" → value: String对象 └── "100"
  • redisDb中的dict:管理所有Redis键,每个键对应一个value对象
  • Hash对象中的dict:每个hash对象内部都有独立的dict,用于存储该hash的field-value对

所以我添加多个hash对象不会冲突,因为每个hash对象都包含自己独立的dict结构。这是一种嵌套的设计,外层dict管理键空间,内层dict(在hash对象中)管理hash的字段。

接下来我们需要知道如何添加数据呢?

即将键值对,添加到dict结构字典中去,Key必须为String对象,Value为任何类型的对象都可以。比如,如果我们使用命令:SEThellomsg"hellomart”,键空间会变成如下结构。

2.2 expires

我们有介绍过,Redis数据都可以设置过期键,这样到了一定的时间,这些对象就会自动过期并回收。那么过期键,又是存储在哪里的呢?过期键是存在expires字典上。

假设上面例子的Key,都设置了过期时间,那么结构如下:

注意,这里的dict中和expires中Key对象,实际都是存储的String对象指针,意思是内存中有一片地址存储这些字符串,而dict和expires中的key实际存储的都是指向内存相应字符串的地址。所以并不是会重复占用内容,Redis对内存的使用都是很珍惜的。

除此之外,redis中如果key设置了ttl,会立马进入到过期字典,要先判断是否过期,再决定是否返回值。而键过期之后不会立即删除,一般会采取三种清楚策略,分别是定时,定期或者惰性删除。Redis主要是定期和惰性删除。

二. 单线程

Redis是一个能高效处理请求的组件,一般而言,对于这种组件我们需要了解它的并发模型是什么样的;而对于Redis,核心处理逻辑Redis一直是单线程的,其它辅助模块也会有一些多线程,比如UNLINK。总之,Redis采用Reactor模式的网络模型,对于一个客户端的请求,主线程负责一个完整的处理过程。

那么为什么Redis会选择单线程呢?

从Redis的定位来看,一般来说Redis的执行会很快,所以执行本身不应该成为瓶颈,而瓶颈通常在网络I/O部分,所以处理逻辑多线程并不会有太大的收益。因为Redis主要是键值存储,其操作主要涉及内存读取/写入和网络通信,而这些操作通常不需要大量的CPU计算,因此多线程带来的CPU并行处理优势并不明显,甚至可能增加线程切换开销。

同时,Redis本身秉持简洁高效的理念,代码的简单性、可维护性是Redis一直以来的追求,引入多线程带来的复杂性远比想象的要大,而且多线程本身也会引入额外成本,下面我们分析一下:

1.多线程引入的复杂性是极大的
首先,多线程引入之后,Redis原来的顺序执行特性就不复存在,为了支持事务的原子性、隔离性,Redis就不得不引入一些很复杂的实现;其次,Redis的数据结构,可以说是极其高效,在单线程模式下做了很多特性的优化,如果引入多线程,那么所有底层数据结构都要改造为线程安全,这会是极其复杂的工作;而且,多线程模式也使得程序调试更加复杂和麻烦,会带来额外的开发成本及运营成本,也更容易犯错

2.多线程带来额外的成本
除了引入复杂度,多线程还会带来额外的成本。包括:

  1. 上下文切换成本,多线程调度需要切换线程上下文,这个操作先存储当前线程的本地数据、程序指针等,然后载入另一个线程数据,这种内核操作的成本不可忽视。
  2. 同步机制的开销,一些公共资源,在单线程模式下直接访问就行了,多线程需要通过加锁等方式去进行同步,这也是不可忽视的CPU开销;
  3. 一个线程本身也占据内存大小,对Redis这种内存数据库而言,内存非常珍贵,多线程本身带来的内存使用的成本也需要谨慎决策。

所以综合来看,多线程其实会带来非常多的成本,如果将处理模块改为多线程,即使在性能上,可能也很难有一个很高的预期,毕竟Redis单线程的处理,已经够快了。

既然我们选择了单线程,为什么单线程能够这么快呢?

我们前面说到,Redis核心的请求处理是单线程,通常来说,单线程的处理能力要比多线程差很多,但是Redis却能使用单线程模型达到每秒数万级别的处理能力,一般业界认知就是Redis的性能是能达到10多w的。这是为什么呢?其实,这是Redis多方面极致设计的一个综合结果。
几个关键点:

  • Redis的大部分操作在内存上完成,内存操作本身就特别快;
  • 第二,Redis追求极致,选择了很多高效的数据结构,并做了非常多的优化,比如ziplist,hash,跳表,有时候一种对象底层有几种实现以应对不同场景。
  • 第三,Redis采用了多路复用机制,使其在网络I0操作中能并发处理大量的客户端请求,实现高吞吐量。

前面两点很好理解,我们这边着重来讲第三点多路复用机制。要理解多路复用机制,我们要先理解为什么要多路复用,没有多路复用情况下,哪些环节可能发生阻塞,Redis是单线程模型,一旦发生阻塞,整体服务都慢会下来。

首先,我们知道Redis是完全在内存中处理数据,所以我们最应该考虑的瓶颈是I/O,我们下面通过分析一次请求,来看一下,一个单线程在一次完整的处理中,哪些地方可能拖慢整个流程。

Redis的服务端在启动的时候,已经bind了端口,并且用listen操作监听客户端请求,此时客户端就可以发起连接请求。此时,客户端发起一次处理请求,比如,客户端发来一个GET请求,服务端需要哪些事情:
1.客户端请求到来时候,使用accept建立连接
2.调用recv从套接字中读取请求
3.解析客户端发送请求,拿到参数
4.处理请求,这里是Get,那么Redis就是通过Key获取对应的数据5.最后将数据通过send发送给客户端

我们要知道,套接字是默认阻塞模式的,这里阻塞可能会发生在两个地方。个是accept,比如accept建立时间过长,另一个是recv时客户端一直没有发送数据。此时,Redis服务就会阻塞在那里。Redis本身定位就是单线程,发生这种阻塞会将整个服务都卡住。所以不能让这两个操作阻塞,这里Redis将套接字设置为非阻塞式的,这样accept和recv都可以非阻塞调用。

非阻塞调用下,如果没数据,不会阻塞在那里,而是让你返回做其它事情。这样可以解决卡死的问题。但我们也需要一种机制,能回过头来看看这些操作是否已经就绪。

最简单的思路,我们可以通过一个循环来不断轮询,但这种方式显然低效。好在各个操作系统实现了一种机制,叫I/O多路复用。

什么叫I/O多路复用,简单理解来说,就是有I/O操作触发的时候,就会产生通知,收到通知,再去处理通知对应的事件,针对I/O多路复用,Redis做了一层包装,叫Reactor模型。

如下图,本质就是监听各种事件,当事件发生时,将事件分发给不同的处理器。

声明: 本篇笔记仅为学习时整理的笔记以及疑问解决点,无其他任何商业用途,如有侵权联系即删。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/7 4:25:25

从零开始:造相-Z-Image 文生图引擎的完整使用手册

从零开始:造相-Z-Image 文生图引擎的完整使用手册 你是否试过输入一段精心打磨的中文提示词,却等来一张全黑、模糊、五官错位的图?是否在RTX 4090显卡上反复调整CFG、步数、采样器,只为让模型别把“穿汉服的女孩”画成“三只手的…

作者头像 李华
网站建设 2026/2/7 22:03:49

Raspberry Pi 4B网络存储NAS构建操作指南

树莓派4B打造静音NAS:一个工程师的实战手记去年冬天,我拆开一台闲置三年的旧笔记本硬盘,想给家里建个能放电影、存照片、自动备份手机相册的小型存储中心。没买成品NAS,也没折腾云盘——就拿手边那块吃灰的树莓派4B 4GB版&#xf…

作者头像 李华
网站建设 2026/2/7 22:04:32

arm版win10下载:高通Snapdragon平台适配完整指南

ARM版Win10下载?别急着点“保存”,先读懂这背后的整套硬件信任链 你搜到的“arm版win10下载”链接,大概率不是一扇通往自由安装的大门,而是一条被精心设限的单行道——它只通向微软认证设备的固件边界之内。这不是一句危言耸听&am…

作者头像 李华
网站建设 2026/2/8 1:35:41

电压模式控制环路:波特图仿真与参数优化

电压模式控制环路:不是“调个电容就完事”,而是用波特图把稳定性刻进电源的DNA里你有没有遇到过这样的场景:- 一块刚焊好的Buck模块,空载稳得像钟表,一加1A负载,输出就“噗”地抖三下;- 某款工业…

作者头像 李华
网站建设 2026/2/8 1:17:07

PCB电镀与蚀刻工艺解析:硬件制造核心原理深度剖析

PCB电镀与蚀刻:硬件工程师绕不开的“物理真相”你有没有遇到过这样的情况?原理图逻辑完美,仿真眼图张得像笑脸,信号完整性(SI)报告绿得发亮——可板子一回来,10 Gbps SerDes链路眼图直接闭合&am…

作者头像 李华
网站建设 2026/2/6 0:02:54

USB3.2速度在Linux系统下的性能验证

USB3.2速度在Linux下的真实性能:从链路协商到内核调度的全栈拆解 你有没有遇到过这样的场景? 手握一块标称“20Gbps”的USB3.2 Gen2x2移动固态硬盘,插进一台高端笔记本, lsusb -t 显示确实是 20000M , dmesg 里…

作者头像 李华