1.redis源码阅读--跳表解析
2.redis源码学习-quicklist篇
3.[redis 源码源码走读] sentinel 哨兵 - 脑裂处理方案
4.Redis7.0源码阅读:哈希表扩容、缩容以及rehash
5.redis源码解读(一):事件驱动的学院io模型,为什么,源码是学院什么,怎么做
6.[redis 源码源码走读] maxmemory 数据淘汰策略
redis源码阅读--跳表解析
跳表是 Redis 中实现 zset 和 set 功能的关键数据结构。通过在链表基础上构建多级索引,学院掌上信贷之家源码跳表有效提升了查找效率,源码且其实现相较于红黑树更为简洁,学院无需大量精力来维持树的源码平衡。跳表节点具有顺序排列的学院特性,支持范围查询。源码
跳表的学院构成包括头结点、尾节点、源码长度以及索引层数。学院每一个节点包含数据 robj、源码分数 score 用于排序、上一节点指针 prev 用于反向遍历,以及多层索引信息 levels。各层索引 skiplistlevel 包括该层索引中节点指向的下一个节点指针 next 和间隔 span。节点的索引层数通过随机数生成,设计思路为使用第 n 级索引是使用第 n-1 级索引概率的 1/4,最多使用 级索引。使用如此设计可确保即便用到最高层级,所持数据量也足够大,无需担心索引不足。
跳表按照 score 和 robj 的大小进行排序,因此节点有序,支持范围查找。插入节点时,首先找到新节点可以插入的位置,即比新节点小的最大节点。此过程从最高层索引开始,使用 update 数组记录各层索引中节点的前一节点位置,以及 rank 数组记录 update 节点到 header 的间隔 span。新节点插入后,更新 prev 指针、tail 指针、跳表长度等信息。
删除节点同样遵循类似的逻辑,先查找节点的前一个节点,然后删除目标节点。在删除过程中,需要检查节点的下一节点是否为待删除数据,并调整节点连接和更新跳表的 level 值。当某层索引中节点的 next 指针变为 nil 时,该层索引已无用,可将 level 减一。最后,更新跳表长度。
虽然跳表概念看似复杂,但通过理解其多级索引机制,其余操作如范围查询、dnfbox封包源码排名查询等将变得相对简单。在实际应用中,可通过阅读 Redis 源码中的 t_zset.c 和 redis.h 文件,了解跳表的具体实现。然而,更难的是将这些抽象概念转化为清晰、易于理解的文档,绘制图表对于深入理解跳表的逻辑非常有帮助。
redis源码学习-quicklist篇
Redis源码中的quicklist是ziplist优化版的双端链表,旨在提高内存效率和操作效率。ziplist虽然内存使用率高,但查找和增删操作的最坏时间复杂度可能达到O(n^2),这与Redis高效数据处理的要求不符。quicklist通过每个节点独立的ziplist结构,降低了更新复杂度,同时保持了内存使用率。
quicklist的基本结构包括:头节点(head)、尾节点(tail)、entry总数(count)、节点总数(len)、容量指示(fill)、压缩深度(compress)、以及用于内存管理的bookmarks。节点结构包括双向链表的prev和next,ziplist的引用zl,ziplist的字节数sz、item数count、以及ziplist类型(raw或lzf压缩)和尝试压缩标志(attempted_compress)。
核心操作函数如create用于初始化节点,insert则根据需求执行头插法或尾插法。delete则简单地从链表中移除节点,释放相关内存。quicklist的优化重点在于ziplist,理解了ziplist的工作原理,quicklist的数据结构理解就相对容易了。
[redis 源码走读] sentinel 哨兵 - 脑裂处理方案
哨兵模式的 Redis 集群在部署时可能出现脑裂现象,即产生多个主服务导致数据不一致的情况。哨兵通过检查、发现故障并进行故障转移来维护集群的高可用性。合理部署配置哨兵和主服务可以有效降低脑裂现象。配置哨兵节点个数和选举法定人数,确保多个哨兵能进行相互选举,选出领导者哨兵进行故障转移,法定人数一般建议为哨兵总数的一半以上,以实现少数服从多数的决策。对于主服务,通过修改配置,当主服务与一定数量的副本失去联系时,禁止客户端向故障主服务进行写操作,从而避免数据不一致的情况。解决此问题时,kotlin 源码eclipse需注意配置选项min-slaves-to-write,其依赖于副本的链接个数,合理设置以确保集群的故障转移能力。高版本的 Redis 已对相关选项进行了优化。总之,通过合理部署哨兵和主服务配置,可以有效管理 Redis 集群,减少脑裂现象带来的问题。
Redis7.0源码阅读:哈希表扩容、缩容以及rehash
当哈希值相同发生冲突时,Redis 使用链表法解决,将冲突的键值对通过链表连接,但随着数据量增加,冲突加剧,查找效率降低。负载因子衡量冲突程度,负载因子越大,冲突越严重。为优化性能,Redis 需适时扩容,将新增键值对放入新哈希桶,减少冲突。
扩容发生在 setCommand 部分,其中 dictKeyIndex 获取键值对索引,判断是否需要扩容。_dictExpandIfNeeded 函数执行扩容逻辑,条件包括:不在 rehash 过程中,哈希表初始大小为0时需扩容,或负载因子大于1且允许扩容或负载因子超过阈值。
扩容大小依据当前键值对数量计算,如哈希表长度为4,实际有9个键值对,扩容至(最小的2的n次幂大于9)。子进程存在时,dict_can_resize 为0,反之为1。fork 子进程用于写时复制,确保持久化操作的稳定性。
哈希表缩容由 tryResizeHashTables 判断负载因子是否小于0.1,条件满足则重新调整大小。此操作在数据库定时检查,且无子进程时执行。
rehash 是为解决链式哈希效率问题,通过增加哈希桶数量分散存储,减少冲突。dictRehash 函数完成这一任务,移动键值对至新哈希表,使用位运算优化哈希计算。渐进式 rehash 通过分步操作,减少响应时间,适应不同负载情况。hibernate源码在哪定时任务检测服务器空闲时,进行大步挪动哈希桶。
在 rehash 过程中,数据查询首先在原始哈希表进行,若未找到,则在新哈希表中查找。rehash 完成后,哈希表结构调整,原始表指向新表,新表内容返回原始表,实现 rehash 结果的整合。
综上所述,Redis 通过哈希表的扩容、缩容以及 rehash 动态调整哈希桶大小,优化查找效率,确保数据存储与检索的高效性。这不仅提高了 Redis 的性能,也为复杂数据存储与管理提供了有力支持。
redis源码解读(一):事件驱动的io模型,为什么,是什么,怎么做
Redis作为一个高性能的内存数据库,因其出色的读写性能和丰富的数据结构支持,已成为互联网应用不可或缺的中间件之一。阅读其源码,可以了解其内部针对高性能和分布式做的种种设计,包括但不限于reactor模型(单线程处理大量网络连接),定时任务的实现(面试常问),分布式CAP BASE理论的实际应用,高效的数据结构的实现,其次还能够通过大神的代码学习C语言的编码风格和技巧,让自己的代码更加优雅。
下面进入正题:为什么需要事件驱动的io模型
我们可以简单地将一个服务端程序拆成三部分,接受请求->处理请求->返回结果,其中接收请求和处理请求便是我们常说的网络io。那么网络io如何实现呢,首先我们介绍最基础的io模型,同步阻塞式io,也是很多同学在学校所学的“网络编程”。
使用同步阻塞式io的单线程服务端程序处理请求大致有以下几个步骤
其中3,4步都有可能使线程阻塞(6也会可能阻塞,这里先不讨论)
在第3步,如果没有客户端请求和服务端建立连接,那么服务端线程将会阻塞。如果redis采用这种io模型,那主线程就无法执行一些定时任务,比如过期key的清理,持久化操作,集群操作等。
在第4步,如果客户端已经建立连接但是没有发送数据,服务端线程会阻塞。hikaricp 2.5.1 源码若说第3步所提到的定时任务还可以通过多开两个线程来实现,那么第4步的阻塞就是硬伤了,如果一个客户端建立了连接但是一直不发送数据,服务端便会崩溃,无法处理其他任何请求。所以同步阻塞式io肯定是不能满足互联网领域高并发的需求的。
下面给出一个阻塞式io的服务端程序示例:
刚才提到,阻塞式io的主要问题是,调用recv接收客户端请求时会导致线程阻塞,无法处理其他客户端请求。那么我们不难想到,既然调用recv会使线程阻塞,那么我们多开几个几个线程不就好了,让那些没有阻塞的线程去处理其他客户端的请求。
我们将阻塞式io处理请求的步骤改造下:
改造后,我们用一个线程去做accept,也就是获取已经建立的连接,我们称这个线程为主线程。然后获取到的每个连接开一个新的线程去处理,这样就能够将阻塞的部分放到新的线程,达到不阻塞主线程的目的,主线程仍然可以继续接收其他客户端的连接并开新的线程去处理。这个方案对高并发服务器来说是一个可行的方案,此外我们还可以使用线程池等手段来继续优化,减少线程建立和销毁的开销。
将阻塞式io改为多线程io:
我们刚才提到多线程可以解决并发问题,然而redis6.0之前使用的是单线程来处理,之所以用单线程,官方给的答复是redis的瓶颈不在cpu,既然不在cpu那么用单线程可以降低系统的复杂度,避免线程同步等问题。如何在一个线程中非阻塞地处理多个socket,进而实现多个客户端的并发处理呢,那就要借助io多路复用了。
io多路复用是操作系统提供的另一种io机制,这种机制可以实现在一个线程中监控多个socket,返回可读或可写的socket,当一个socket可读或可写时再去操作它,这样就避免了对某个socket的阻塞等待。
将多线程io改为io多路复用:
什么是事件驱动的io模型(Reactor)
这里只讨论redis用到的单线程Reactor模型
事件驱动的io模型并不是一个具体的调用,而是高并发服务器的一种抽象的编程模式。
在Reactor模型中,有三种事件:
与这三种事件对应的,有三种handler,负责处理对应的事件。我们在一个主循环中不断判断是否有事件到来(一般通过io多路复用获取事件),有事件到来就调用对应的handler去处理时间。
听着玄乎,实际上也就这一张图:
事件驱动的io模型在redis中的实现
以下提及的源码版本为 5.0.8
文字的苍白的,建议参照本文最后的方法下载代码,自己调试下
整体框架
redis-server的main方法在 src/server.c 最后,在main方法中,首先进行一系列的初始化操作,最后进入进入Reactor模型的主循环中:
主循环在aeMain函数中,aeMain函数传入的参数 server.el ,是一个 aeEventLoop 类型的全局变量,保存了主循环的一些状态信息,包括需要处理的读写事件、时间事件列表,epoll相关信息,回调函数等。
aeMain函数中,我们可以看到当 eventLoop->stop 标志位为0时,while循环中的内容会被重复执行,每次循环首先会调用beforesleep回调函数,然后处理时间。beforesleep函数在main函数中被注册,会进行集群状态更新、AOF落盘等任务。
之所以叫beforesleep,是因为aeProcessEvents函数中包含了获取事件和处理事件的逻辑,其中获取读写事件时通过epoll_wait实现,会将线程阻塞。
在aeProcessEvents函数中,处理读写事件和时间事件,参数flags定义了需要处理的事件类型,我们可以暂时忽略这个参数,认为读写时间都需要处理。
aeProcessEvents函数的逻辑可以分为三个部分,首先获取距离最近的时间事件,这一步的目的是为了确定epoll_wait的超时时间,并不是实际处理时间事件。
第二个部分为获取读写事件并处理,首先调用epoll_wait,获取需要处理的读写事件,超时时间为第一步确定的时间,也就是说,如果在超时时间内有读写事件到来,那么处理读写时间,如果没有读写时间就阻塞到下一个时间事件到来,去处理时间事件。
第三个部分为处理时间事件。
事件注册与获取
上面我们讲了整体框架,了解了主循环的大致流程。接下来我们来看其中的细节,首先是读写事件的注册与获取。
redis将读、写、连接事件用结构aeFileEvent表示,因为这些事件都是通过epoll_wait获取的。
事件的具体类型通过mask标志位来区分。aeFileEvent还保存了事件处理的回调函数指针(rfileProc、wfileProc)和需要读写的数据指针(clientData)。
既然读写事件是通过epoll io多路复用实现,那么就避不开epoll的三部曲 epoll_create epoll_ctrl epoll_wait,接下来我们看下redis对epoll接口的封装。
我们之前提到aeMain函数的参数是一个 aeEventLoop 类型的全局变量,aeEventLoop中保存了epoll文件描述符和epoll事件。在aeApiCreate函数(src/ae_epoll.c)中,会调用epoll_create来创建初始化epoll文件描述符和epoll事件,调用关系为 main -> initServer -> aeCreateEventLoop -> aeApiCreate
调用epoll_create创建epoll后,就可以添加需要监控的文件描述符了,需要监控的情形有三个,一是监控新的客户端连接连接请求,二是监控客户端发送指令,也就是读事件,三是监控客户端写事件,也就是处理完了请求写回结果。
这三种情形在redis中被抽象为文件事件,文件事件通过函数aeCreateFileEvent(src/ae.c)添加,添加一个文件事件主要包含三个步骤,通过epoll_ctl添加监控的文件描述符,指定回调函数和指定读写缓冲区。
最后是通过epoll_wait来获取事件,上文我们提到,在每次主循环中,首先根据最近到达的时间事件来计算epoll_wait的超时时间,然后调用epoll_wait获取事件,再处理事件,其中获取事件在函数aeApiPoll(src/ae_epoll.c)中。
获取到事件后,主循环中会逐个调用事件的回调函数来处理事件。
读写事件的实现
写累了,有空补上……
如何使用vscode调试redis源码
编译出二进制程序
这一步有可能报错:
jemalloc是内存分配的一种更高效的实现,用于代替libc的默认实现。这里报错找不到jemalloc,我们只需要将其替换成libc默认实现就好:
如果报错:
我们可以在src目录找到一个脚本名为mkreleasehdr.sh,其中包含创建release.h的逻辑,将报错信息网上翻可以发现有一行:
看来是这个脚本没有执行权限,导致release.h没有成功创建,我们需要给这个脚本添加执行权限然后重新编译:
2. 创建调试配置(vscode)
创建文件 .vscode/launch.json,并填入以下内容:
然后就可以进入调试页面打断点调试了,main函数在 src/server.c
[redis 源码走读] maxmemory 数据淘汰策略
Redis 是一个内存数据库,通过配置 `maxmemory` 来限定其内存使用量。当 Redis 主库内存超出限制时,会触发数据淘汰机制,以减少内存使用量,直至达到限制阈值。
当 `maxmemory` 配置被应用,Redis 会根据配置采用相应的数据淘汰策略。`volatile-xxx` 类型配置仅淘汰设置了过期时间的数据,而 `allkeys-xxx` 则淘汰数据库中所有数据。若 Redis 主要作为缓存使用,可选择 `allkeys-xxx`。
数据淘汰时机发生在事件循环处理命令时。有多种淘汰策略可供选择,从简单到复杂包括:不淘汰数据(`noeviction`)、随机淘汰(`volatile-random`、`allkeys-random`)、采样淘汰(`allkeys-lru`、`volatile-lru`、`volatile-ttl`、`volatile-freq`)以及近似 LRU 和 LRU 策略(`volatile-lru` 和 `allkeys-lru`)。
`noeviction` 策略允许读操作但禁止大多数写命令,返回 `oomerr` 错误,仅允许执行少量写命令,如删除命令 `del`、`hdel` 和 `unlink`。
`volatile-random` 和 `allkeys-random` 机制相对直接,随机淘汰数据,策略相对暴力。
`allkeys-lru` 策略根据最近最少使用(LRU)算法淘汰数据,优先淘汰最久未使用的数据。
`volatile-lru` 结合了过期时间与 LRU 算法,优先淘汰那些最久未访问且即将过期的数据。
`volatile-ttl` 策略淘汰即将过期的数据,而 `volatile-freq` 则根据访问频率(LFU)淘汰数据,考虑数据的使用热度。
`volatile-lru` 和 `allkeys-lru` 策略通过采样来近似 LRU 算法,维护一个样本池来确定淘汰顺序,以提高淘汰策略的精确性。
总结而言,Redis 的数据淘汰策略旨在平衡内存使用与数据访问需求,通过灵活的配置实现高效的数据管理。策略的选择应基于具体应用场景的需求,如数据访问模式、性能目标等。
Redis源码剖析之数据过期(expire)
通过对线上数据访问时间分布的统计发现,大部分请求只访问最新分钟或1小时的数据,极少访问超过1天的数据。这使得我们在存储数据时可以优化过期时间,例如将过期时间从2天缩短到1天,从而节省大量 Redis 实例资源,节省内存使用量和成本。
Redis 自动清理过期数据的机制可以有效节省资源,而没有自动过期机制时,实现数据清理将非常复杂。自动过期功能不仅简化了操作,还能节省成本,体现了其在缓存系统中的重要性。
Redis 在处理请求时,会检查 key 是否过期。在 dictEntry 结构中存储了上次更新时间戳,通过比较当前时间与更新时间戳之间的差值与设定的过期时间,判断 key 是否过期。
Redis 提供了懒惰删除功能,即在开启配置项后,会异步处理数据删除任务,防止阻塞主线程。然而,实际实现并非完全异步,而是结合了同步和异步机制,以优化性能。
为了解决数据写入后长时间无访问导致的资源占用问题,Redis 实现了定期抽样删除策略。通过单线程执行的核心流程,Redis 无法长时间暂停执行其他工作,因此定期清理时仅做少量操作,以避免长时间阻塞。
Redis 数据过期策略简单,但需考虑性能影响。配置过期时间应根据业务需求和数据特性调整,以实现最佳性能和资源利用。
本文深入探讨了 Redis 过期数据的实现,包括实时清理、惰性删除和定期抽样删除策略。同时提供了 Redis 中文注释版和源码剖析专栏链接,欢迎关注和学习。如有帮助,欢迎一键三连支持。
Redis 源码剖析 3 -- redisCommand
Redis 使用 redisCommand 结构体处理命令请求,其内包含一个指向对应处理函数的 proc 指针。redisCommandTable 是一个存储所有 Redis 命令的数组,位于 server.c 文件中。此数组通过 populateCommandTable() 函数填充,该函数将 redisCommandTable 的内容添加到 server.commands 字典,将 Redis 支持的所有命令及其实现整合。
populateCommandTable() 函数中包含 populateCommandTableParseFlags() 子函数,用于将 sflags 字符串转换为对应的 flags 值。lookupCommand*() 函数族负责从 server.commands 中查找相应的命令。
Redis源码解析:一条Redis命令是如何执行的?
作者:robinhzhang Redis,一个开源内存数据库,凭借其高效能和广泛应用,如缓存、消息队列和会话存储,本文将带你探索其命令执行的底层流程。本文将以源码解析的形式,逐层深入Redis的核心结构和命令执行过程,旨在帮助开发者理解实现细节,提升编程技术和设计意识。源码结构概览
在学习Redis源代码之前,首先要了解其主要的组成部分:redisServer、redisClient、redisDb、redisObject以及aeEventLoop。这些结构体和事件模型构成了Redis的核心架构。redisServer:服务端运行的核心结构,包括监听socket、数据存储的redisDb列表和客户端连接信息。
redisClient:客户端连接状态的存储,包括命令处理缓冲区、回复数据列表和数据库句柄。
redisDb:键值对的数据存储,采用两个哈希表实现渐进式rehash。
redisObject:存储对象的通用表示,包含引用计数和LRU时间,用于内存管理。
aeEventLoop:事件循环,管理文件和时间事件的处理。
核心流程详解
Redis的执行流程从main函数开始,首先初始化配置和服务器组件,进入主循环处理事件。命令执行流程涉及redis启动、客户端连接、接收命令和返回结果四个步骤:启动阶段:创建socket服务器,注册可读事件,进入主循环。
连接阶段:客户端连接后,接收并处理命令,创建客户端实例。
命令阶段:客户端发送命令,服务端解析并调用对应的命令处理函数。
结果阶段:处理命令后,根据协议格式构建回复并写回客户端。
渐进式rehash与内存管理
Redis的内存管理采用引用计数法,通过对象的refcount字段控制内存分配和释放。rehash操作在Redis 2.x版本引入,通过逐步迁移键值对,降低对单线程性能的影响。当负载达到阈值,会进行扩容,这涉及新表的创建和键值对的迁移。总结
本文通过Redis源码分析,揭示了其命令执行的细节,包括启动流程、客户端连接、命令处理和结果返回,以及内存管理策略。这将有助于开发者深入理解Redis的工作原理,提升编程效率和设计决策能力。