redis 源码学习
一直实践以来, redis 的性能很高,且内存占用很小。一直想知道是为什么?这次看 redis 源码,首先就奔着这一点去看了。
redis 底层存储原理
使用了 sds 进行存储,其实他就是 c 的 char 指针,所以说其和 c 是无缝贴合的。
但是它不在由 c 语言进行管理,而是自定义结构体,里面是长度、可用大小,flag 以及数据。
1 | typedef char *sds; |
它的结构很简单,长度,分配的空间,标示和内容。redis 里面涉及到 char 的一般都会用指针找到位置,然后赋值。
实际存储在 db dic 里面的 是 redisObject。里面存储了类型、编码、过期算法、连接数和内容即 sds。
1 | typedef struct redisObject { |
看起来很简单,但它通过一系列措施来确保高效。
比如如果长度小于等于 44,那么就会保存不变区域的 string。具体是因为 1+3+4+8 是 16 个字节,再加上 sds 的最小 3 个字节,还有一个 ‘\0’ 结尾字节,在 64 字节(小字节下避免空间浪费)下得出 44。内部实现会让 redisObject 和 sds 内存一块申请。
append 的时候会检查,如果没有,正常创建。有的话会尝试转为 raw 类型。
总结来看:
- 扩容。凭借指针操作和记录的当前 len 和申请 alloc ,可以快速进行判断决定是扩展内存还是直接就申请新的内存。
- 内容利用。redis 对小数据做了很多的优化,比如申请一次内存、int 类型直接存储、共享创建等等
- 精准内存操作。会尽量到位到具体的内存地址,然后直接操作赋值或者复制等等。
启动流程
官方文档写的挺清晰的,server.c main 内部主要方法是 initServerConfig()
-> initServer()
-> aeMain(server.el)
。redis 内部真的大量封装了原生的方法,利用宏编译来尽可能地提高在各个平台的性能。比如在 zmalloc 内部进行自己的内存管理,根据各个平台函数提供调用来确保分配的内存和内存大小明确。
注册 file event 回调
initServer
-> createSocketAcceptHandler
-> aeCreateFileEvent
里面会注册 event handler,比如 acceptTcpHandler 内部会取出数据,然后注册 acceptCommonHandler,conn 状态变为 CONN_STATE_ACCEPTING
1 | typedef enum { |
开始 eventloop
aeMain(server.el)
-> aeProcessEvents
内部会 aeApiPoll
-> rfileProc
即前面注册的回调调用处理 file event。 以 acceptTcpHandler
为例,先连接 client socket fd,然后进行读取且 conn 状态变为 CONN_STATE_ACCEPTING
;acceptTcpHandler
之后调用 acceptCommonHandler
创建 client,将状态变为 CONN_STATE_CONNECTED
,执行 clientAcceptHandler
方法,读取成功后调用 moduleFireServerEvent
来进行执行之前注册到 RedisModule_EventListeners
的 callback。
那么它们是怎么和命令处理关联起来的。
在 acceptCommonHandler
方法创建 client 的时候会创建 createClient,在创建过程中会通过 connSetReadHandler
调用 connSocketSetReadHandler
注册 readQueryFromClient
方法。 而 connSetReadHandler
方法内部是关键,它会readQueryFromClient
方法赋值 conn->read_handler,然后调用 aeCreateFileEvent
创建事件,mark 是 AE_READABLE,回调是 .ae_handler(connSocketEventHandler),其内部会根据连接情况和最后的处理分别调用方法。InitServerLast
-> initThreadedIO
-> IOThreadMain
-> readQueryFromClient
-> processInputBuffer
-> processCommandAndResetClient
。
总结一下,可以分为几个步骤:
- socket 监听,调用系统的方法进行监听,然后进行封装打标记,并赋值 eventLoop->fired,返回数量。
- eventloop 根据数量遍历,从 fired 拿到相应 fd、mark 信息,然后回调一开始初始化设置的监听 accept aeFileProc,不管哪种如 ip 或者 tls 等,最后都会调用 acceptCommonHandler,该方法作用在上面详细说明了。它最终会产生 readQueryFromClient 事件。
- readQueryFromClient 会解析客户端发起的参数,解析,然后进行命令的调用,它会调用一开始 populateCommandTable 方法注册的命令(redisCommand redisCommandTable[] 里读取)。
- 以 get 为例,会从 db 的 dic 查询 key 的值(db 默认为 0 ,除非 select 切换)。
redis 内部举例
list 用的 quicklist,本质上就是双向链表,但是它内部还兼容了 ziplist,quicklist 内部的 node 会有 char 数组来存储压缩的内容。
结论
- redis 主要是充分利用内存会让缓存达到很高的速度,在辅助一些内存优化即可实现高速缓存。
- redis 内部的单线程来确保避免了线程的切换损耗,网络 IO 这块用多线程会有更好的性能。
- 启发,其实很多时候程序的瓶颈都在内存、网络、存储等等,cpu 存在不够的情况很低,cpu 不够如果不是因为高计算量,其实优化空间很大。所以在我们开发的时候重点关注那些会产生 IO 的操作。