1.redis源码解读(一):事件驱动的码解io模型,为什么,码解是码解什么,怎么做
2.万字长文带你解读Redisson分布式锁的码解源码
3.Redis 主从复制 - 源码梳理
4.Redis源码阅读(1)——zmalloc
5.Redis 源码剖析 3 -- redisCommand
6.读懂Redis:从源码分析其跳表实现
redis源码解读(一):事件驱动的io模型,为什么,码解是码解源码空间招聘什么,怎么做
Redis作为一个高性能的码解内存数据库,因其出色的码解读写性能和丰富的数据结构支持,已成为互联网应用不可或缺的码解中间件之一。阅读其源码,码解可以了解其内部针对高性能和分布式做的码解种种设计,包括但不限于reactor模型(单线程处理大量网络连接),码解定时任务的码解实现(面试常问),分布式CAP BASE理论的码解实际应用,高效的码解数据结构的实现,其次还能够通过大神的代码学习C语言的编码风格和技巧,让自己的代码更加优雅。
下面进入正题:为什么需要事件驱动的io模型
我们可以简单地将一个服务端程序拆成三部分,接受请求->处理请求->返回结果,其中接收请求和处理请求便是我们常说的网络io。那么网络io如何实现呢,首先我们介绍最基础的io模型,同步阻塞式io,也是很多同学在学校所学的“网络编程”。
使用同步阻塞式io的单线程服务端程序处理请求大致有以下几个步骤
其中3,4步都有可能使线程阻塞(6也会可能阻塞,这里先不讨论)
在第3步,如果没有客户端请求和服务端建立连接,那么服务端线程将会阻塞。如果redis采用这种io模型,那主线程就无法执行一些定时任务,比如过期key的清理,持久化操作,集群操作等。
在第4步,如果客户端已经建立连接但是没有发送数据,服务端线程会阻塞。若说第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函数传入的星海app源码参数 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
万字长文带你解读Redisson分布式锁的源码
通过深入解读 Redisson 分布式锁的源码,我们了解到其核心功能在于实现加锁、解锁以及设置锁超时这三个基本操作。自己写网站源码而分布式锁的实现,离不开对 Redis 发布订阅(pub/sub)机制的利用。订阅者(sub)通过订阅特定频道(channel)来接收发布者(pub)发送的消息,实现不同客户端间的通信。在使用 Redisson 加锁前,需获取 RLock 实例对象,进而调用 lock 或 tryLock 方法来完成加锁过程。
Redisson 中的 RLock 实例初始化时,会配置异步执行器、唯一 ID、等待获取锁的时间等参数。加锁逻辑主要涉及尝试获取锁(tryLock)和直接获取锁(lock)两种方式。tryLock 方法中,通过尝试获取锁并监听锁是否被释放来实现锁的获取和等待逻辑。这通过调用底层命令(整合成 Lua 脚本)与 Redis 进行交互来实现。Redis 的 Hash 结构被用于存储锁的持有情况,hincrby 命令用于在持有锁的线程释放锁时调整计数,确保锁的可重入性。
解锁逻辑相对简单,通过调用 unlock 方法,Redisson 使用特定的 Lua 脚本命令来判断锁是否存在,是否为当前线程持有,并相应地执行删除或调整锁过期时间的操作。
此外,Redisson 支持 RedLock 算法来提供一种更鲁棒的锁实现,通过多个无关联的 Redis 实例(Node)组成的分布式锁来防止单点故障。尽管 RedLock 算法能一定程度上提高系统可靠性,但并不保证强一致性。因此,在业务场景对锁的安全性有较高要求时,可采取业务层幂等处理作为补充。
Redisson 的设计遵循了简化实现与高效性能的原则,通过 Lua 脚本与 Redis 的直接交互来实现分布式锁的原子操作。在源码中,通过巧妙利用并发工具和网络通信机制,实现了分布式锁的高效执行。尽管 Redisson 在注释方面可能稍显不足,但其源码中蕴含的并发与网络通信的最佳实践仍然值得深入学习与研究。
Redis 主从复制 - 源码梳理
本文主要剖析Redis主从复制机制中的核心组件之一——复制积压缓冲区(Replication Buffer),旨在为读者提供一个对Redis复制流程和缓冲区机制深入理解的平台,以下内容仅基于Redis版本7.0.,若读者在使用过程中发现偏差,欢迎指正。
复制积压缓冲区在逻辑上可理解为一个容量最大的位整数,其初始值为1,由offset、master_repl_offset和repl_backlog-histlen三个变量共同决定缓冲区的有效范围。offset表示缓冲区内命令起始位置,master_repl_offset代表结束位置,二者之间的长度由repl_backlog-histlen表示。
每当主节点执行写命令,新生成的积压缓冲区大小增加,同时增加master_repl_offset和repl_backlog-histlen的值,直至达到预设的最大容量(默认为1MB)。一旦所有从节点接收到命令并确认同步无误,缓冲区内过期的命令将被移除,并调整offset和histlen以维持积压区容量的稳定性。
为实现动态分配,复制积压缓冲区被分解成多个block,以链表形式组织。每个block采用引用计数管理策略,初始值为0,每当增加或删除从节点对block的引用时,计数值相应增减。新生成block时,将master_repl_offset+1设置为block的repl_offset值,并将写入命令拷贝至缓冲区内,与此同时,master_repl_offset和repl_backlog-histlen增加。
通过循环遍历所有从节点,为每个从节点设置ref_repl_buf_node指向当前block或最后一个block,确保主从复制能够准确传递命令。当主节点接收到从节点的连接请求时,将开始填充积压缓冲区。在全量复制阶段,从slave-replstate为WAIT_BGSAVE_START至ONLINE,表示redis从后台进程开始执行到完成RDB文件传输和加载,命令传播至此阶段正式开始。
针对每个从节点,主节点从slave-ref_block_pos开始发送积压缓冲区内的命令,每发送成功,slave-ref_block_pos相应更新。当积压缓冲区超过预设阈值,即复制积压缓冲区中的有效长度超过repl-backlog-size(默认1MB)时,主节点将清除已发送的缓冲区,释放内存。如果主节点写入命令频繁或从节点断线重连时间长,则需合理调整缓冲区大小(推荐值为2 * second * write_size_per_second)以保持增量复制的稳定运行。
当最后一个从节点与主节点的连接断开超过repl-backlog-ttl(默认为秒)时,主节点将释放repl_backlog和复制积压缓冲区以确保资源的有效使用。不过需要注意的是,从节点的释放操作依赖于节点是否可能成为新的主节点,因此在最后处理逻辑上需保持谨慎。
Redis源码阅读(1)——zmalloc
zmalloc是一个简化内存分配的库,包含以下API函数:zmalloc
zcalloc
zrealloc
zfree
zstrdup
zmalloc_used_memory
zmalloc_set_oom_handler
zmalloc_get_rss
zmalloc_get_allocator_info
zmalloc_get_private_dirty
zmalloc_get_smap_bytes_by_field
zmalloc_get_memory_size
zlibc_free
其中,zmalloc用于分配内存,zcalloc在分配内存的同时初始化为0,zrealloc用于重新分配内存,zfree用于释放内存,zstrdup用于复制字符串并分配内存,zmalloc_used_memory用于获取已分配内存的大小,zmalloc_set_oom_handler用于设置内存溢出处理器,zmalloc_get_rss用于获取当前进程的内存使用量,zmalloc_get_allocator_info用于获取分配器信息,zmalloc_get_private_dirty用于获取私有脏数据,zmalloc_get_smap_bytes_by_field用于获取指定字段的内存使用量,zmalloc_get_memory_size用于获取内存大小,zlibc_free用于释放内存。 在zmalloc中,宏函数update_zmalloc_stat_alloc用于更新used_memory的值。这个宏函数中的if语句用于补齐分配的内存字节数到sizeof(long),但是我不太理解5.0源码中为什么atomicIncr使用的是__n而不是直接对_n进行操作。测试发现,used_memory的值并未对齐到8,那么if语句的存在意义何在呢? 同样地,update_zmalloc_stat_free宏函数用于更新已释放内存的统计信息。与update_zmalloc_stat_alloc相比,虽然malloc_usable_size已经返回精确的字节数,但update_zmalloc_stat_alloc为何不直接使用atomicIncr更新used_memory呢?在Unstable分支中,已有开发者对此进行了优化。Redis 源码剖析 3 -- redisCommand
Redis 使用 redisCommand 结构体处理命令请求,其内包含一个指向对应处理函数的 proc 指针。redisCommandTable 是一个存储所有 Redis 命令的数组,位于 server.c 文件中。此数组通过 populateCommandTable() 函数填充,该函数将 redisCommandTable 的内容添加到 server.commands 字典,将 Redis 支持的所有命令及其实现整合。
populateCommandTable() 函数中包含 populateCommandTableParseFlags() 子函数,用于将 sflags 字符串转换为对应的 flags 值。lookupCommand*() 函数族负责从 server.commands 中查找相应的命令。
读懂Redis:从源码分析其跳表实现
要深入理解Redis中跳表的奥秘,首先,我们从理想化的跳表概念开始。跳表作为一种多层级有序链表,旨在提供高效的有序集合操作,如zrange和zrevrange。它的设计旨在通过空间换时间,以O(log_2 n)的时间复杂度进行查找,但删除和增加操作可能导致结构变动,这在理想情况下需要复杂的重构。
Redis在实践中对跳表进行了优化,以牺牲一定程度的复杂性来节省内存。它限制了跳表的最高层级为,并根据节点数量和字符串长度选择是否使用跳表。Redis的跳表设计重点在于第一个层级的元素,这使得范围查询极其高效,而这是其他数据结构难以比拟的特性。
当添加新元素到zset对象时,会根据特定条件(zset_max_ziplist_entries和zset_max_ziplist_value)决定是否转换为跳表。通过配置Redis的配置文件,用户可以调整这些参数以适应不同的需求。
总的来说,Redis的跳表实现是内存与性能之间的一种平衡,它在有序集合操作中发挥着关键作用,同时为高效查询提供了基础。对于希望系统学习C/C++、Linux系统和深入理解高性能存储的读者,可以关注我们的公众号《Lion 莱恩呀》获取更多技术内容,包括白金学习卡,覆盖基础架构、golang云原生等领域。
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的工作原理,提升编程效率和设计决策能力。Redis radix tree 源码解析
Redis 实现了不定长压缩前缀的 radix tree,用于集群模式下存储 slot 对应的所有 key 信息。本文解析在 Redis 中实现 radix tree 的核心内容。
核心数据结构的定义如下:
每个节点结构体 (raxNode) 包含了指向子节点的指针、当前节点的 key 的长度、以及是否为叶子节点的标记。
以下是插入流程示例:
场景一:仅插入 "abcd"。此节点为叶子节点,使用压缩前缀。
场景二:在 "abcd" 之后插入 "abcdef"。从 "abcd" 的父节点遍历至压缩前缀,找到 "abcd" 空子节点,插入 "ef" 并标记为叶子节点。
场景三:在 "abcd" 之后插入 "ab"。ab 为 "abcd" 的前缀,插入 "ab" 为子节点,并标记为叶子节点。同时保留 "abcd" 的前缀结构。
场景四:在 "abcd" 之后插入 "abABC"。ab 为前缀,创建 "ab" 和 "ABC" 分别为子节点,保持压缩前缀结构。
删除流程则相对简单,找到指定 key 的叶子节点后,向上遍历并删除非叶子节点。若删除后父节点非压缩且大小大于1,则需处理合并问题,以优化树的高度。
合并的条件涉及:删除节点后,检查父节点是否仍为非压缩节点且包含多个子节点,以此决定是否进行合并操作。
结束语:云数据库 Redis 版提供了稳定可靠、性能卓越、可弹性伸缩的数据库服务,基于飞天分布式系统和全SSD盘高性能存储,支持主备版和集群版高可用架构。提供全面的容灾切换、故障迁移、在线扩容、性能优化的数据库解决方案,欢迎使用。
2024-12-23 01:29
2024-12-23 01:24
2024-12-23 01:06
2024-12-23 01:06
2024-12-23 00:37
2024-12-23 00:34
2024-12-22 23:14
2024-12-22 23:04