Redis设计和源码那些事儿,带你一步步看透底层原理和架构细节
- 问答
- 2026-01-04 05:31:15
- 10
主要参考自黄健宏的《Redis设计与实现》以及Redis官方文档和源码解读文章)
咱们先从Redis最核心的东西说起,它不是那些花哨的数据类型,而是一个最简单也最关键的数据库服务是怎么跑起来的,你想想,你启动一个Redis服务器,它就在那儿等着,你怎么能通过网络给它发命令,它又怎么给你回结果呢?这就是事件驱动模型要解决的事儿。
Redis自己造了一个轻量级的事件库,核心是两个东西:文件事件和时间事件。(来源:《Redis设计与实现》第12章)文件事件说白了就是处理网络连接,比如有个客户端通过TCP连过来了,或者客户端发来了一个命令数据包,这些都被看作是文件事件,时间事件呢,就像是闹钟,比如每隔100毫秒检查一下有没有过期的key,或者做一下持久化的操作。

Redis的主线程就是一个大循环,这个循环不停地跑,它的工作就是去问操作系统:“有没有什么新的事件发生啊?”(这个过程叫事件轮询,Redis早期用select,后来用更高效的epoll),如果有客户端发来了一个“set name redis”的命令,那么文件事件就触发了,主线程就会把这个命令读进来,然后找对应的处理函数来执行,最后再把结果“OK”写回给客户端,关键点在于,这一切都是在这个主线程里顺序执行的,所以Redis是单线程的,你可能会问,单线程不会慢吗?Redis的想法是,对于内存操作来说,真正的瓶颈往往是内存大小和网络带宽,而不是CPU,单线程避免了多线程带来的加锁解锁的麻烦,反而让实现变得简单高效。(来源:Redis作者Salvatore Sanfilippo的博客观点)
好,现在有客户端连上来发命令了,那发的命令到底是什么数据结构存在内存里的呢?这就引出了Redis的“万能钥匙”——RedisObject。(来源:《Redis设计与实现》第2章)Redis里所有的key和value都不是直接存个字符串或者链表就完事儿了,而是被封装在一个叫RedisObject的结构体里,这个结构体就像是一个礼物的包装盒,里面有几个重要的标签:

- type(类型):标明里面装的是什么数据类型,是字符串(string)、列表(list)、哈希(hash)、集合(set)还是有序集合(zset),你执行
type key命令,看的就是这个标签。 - encoding(编码):这是最体现Redis优化精神的地方,它标明这个数据在底层到底是用哪种具体数据结构实现的,比如同样是字符串,如果很短,可能就用一种更省空间的embstr编码;如果很长,就用简单的动态字符串(SDS),同样是列表,如果元素少且小,可能就用压缩列表(ziplist),这样内存连续,节省空间;如果元素多了,就升级成标准的双向链表(linkedlist),这样修改效率更高,这种设计让Redis可以根据实际情况灵活选择最合适的底层结构,在速度和空间上做权衡。
- lru:这个字段记录了这个key最后一次被访问的时间,是用来做近似LRU内存淘汰算法的依据,当内存满了的时候,Redis就靠这个信息来决定把哪些不常用的key踢出去。
- refcount(引用计数):这就是Redis实现内存回收的方式,类似于智能指针的原理,当一个key被创建时,refcount为1,如果又被别的程序引用了,就加1,当refcount减到0时,就说明这个对象彻底没人用了,内存就可以放心回收了。
- ptr指针:这个指针才真正指向存放数据的具体数据结构,比如指向一个SDS,或者一个压缩列表,或者一个跳表。
举个例子,你执行set name "a very long long long string...",Redis就会创建一个RedisObject,type是STRING,encoding根据长度可能是RAW(即SDS),然后ptr指向存有那个长字符串的SDS结构。
说到字符串,就不得不提Redis的字符串实现——SDS(Simple Dynamic String,简单动态字符串)。(来源:《Redis设计与实现》第2章)它可不是C语言里那个以\0结尾的普通字符串,SDS有自己的优点:

- 常数复杂度获取字符串长度:因为SDS结构里直接存了字符串的长度(len字段),所以
strlen命令直接返回这个值就行了,是O(1)操作,而C字符串需要遍历整个字符串才能算出长度。 - 杜绝缓冲区溢出:C语言里用
strcat函数拼接字符串,如果目标空间不够就会覆盖别的内容,SDS在拼接前会检查空间是否足够,不够的话会自动扩容。 - 减少修改字符串时带来的内存重分配次数:SDS采用了“预分配”和“惰性空间释放”的策略,比如你给SDS追加内容,它不光会分配刚好够用的空间,还会多分配一些空闲空间(free字段),这样下次再追加时,如果空闲空间够用,就不用再麻烦操作系统分配内存了,提升了性能。
再来看看常用的哈希表(hash)。(来源:《Redis设计与实现》第4章)Redis的整个数据库本身就是一个巨大的“字典”(key-value映射),这个字典就是用哈希表实现的,当你执行hset user:1 name john age 30时,这个"user:1"这个key对应的value(一个哈希表)里面,"name"和"john"形成了一个键值对,"age"和"30"形成了另一个键值对。
哈希表的核心是“渐进式rehash”,因为随着数据增多,哈希冲突会变严重,需要扩容(扩大哈希数组的大小)来保持效率,但如果你有几百万个key,一次性把所有key从旧表迁移到新表,会导致Redis服务卡顿很久,Redis的解决办法很巧妙:它准备两个表,旧表和新表,当需要扩容时,它并不一次性搬完,而是在后续的每次增删改查命令中,顺便迁移旧表里的一个或几个key到新表,这样就把庞大的迁移工作打散成无数个小任务,慢慢完成,避免了服务停顿,在这个过程中,查找key会同时查两个表。
最后简单提一下持久化。(来源:Redis官方文档)Redis的数据在内存里,要想重启不丢数据,就得存到磁盘上,这就是持久化,主要有两种方式:
- RDB(快照):在某个时间点,把整个数据库的状态像拍照片一样,完整地保存到一个压缩的二进制文件(dump.rdb)里,恢复的时候直接读进内存就行,很快,你可以配置成每隔一段时间(比如5分钟内有100次写操作)就自动拍一次快照,它的缺点是可能会丢失最后一次快照到宕机期间的数据。
- AOF(追加日志):把每一次写命令都记录到一个日志文件里(appendonly.aof),恢复的时候就把日志里的命令重新执行一遍,这样数据更安全,最多丢失一秒的数据(如果配置为每秒同步一次磁盘),但AOF文件通常会比RDB文件大,而且恢复速度慢。
在实际生产中,往往两者结合使用,用AOF来保证数据不丢失,用RDB来做冷备份和快速恢复。
看Redis源码,其实就是把这些设计上的点,对应到src目录下的具体C语言文件,比如ae.c是事件循环,sds.h是SDS的实现,dict.c是哈希字典的实现,object.c是RedisObject的实现,理解了这些核心设计和架构,再去看源码,就会觉得脉络清晰很多。
本文由寇乐童于2026-01-04发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/74141.html
