赞
踩
目录
结构源码:
- struct sdshdr{
- //记录buf数组中已使用字节的数量
- //等于 SDS 保存字符串的长度
- int len;
- //记录 buf 数组中未使用字节的数量
- int free;
- //字节数组,用于保存字符串
- char buf[];
- }
图例:
SDS与C字符串对比
ListNode节点数据结构:
- typedef struct listNode{
- //前置节点
- struct listNode *prev;
- //后置节点
- struct listNode *next;
- //节点的值
- void *value;
- }listNode
链表数据结构:
- typedef struct list{
- //表头节点
- listNode *head;
- //表尾节点
- listNode *tail;
- //链表所包含的节点数量
- unsigned long len;
- //节点值复制函数
- void (*free) (void *ptr);
- //节点值释放函数
- void (*free) (void *ptr);
- //节点值对比函数
- int (*match) (void *ptr,void *key);
- }list;
特点:
哈希表:
- typedef struct dictht {
- // 哈希表数组
- dictEntry **table;
- // 哈希表大小
- unsigned long size;
- // 哈希表大小掩码,用于计算索引值
- // 总是等于 size - 1
- unsigned long sizemask;
- // 该哈希表已有节点的数量
- unsigned long used;
- } dictht;
Hash表节点:
- typedef struct dictEntry {
- // 键
- void *key;
- // 值
- union {
- void *val;
- uint64_t u64;
- int64_t s64;
- } v;
- // 指向下个哈希表节点,形成链表
- struct dictEntry *next; // 单链表结构
- } dictEntry;
字典:
- typedef struct dict {
- // 类型特定函数
- dictType *type;
- // 私有数据
- void *privdata;
- // 哈希表
- dictht ht[2];
- // rehash 索引
- // 当 rehash 不在进行时,值为 -1
- int rehashidx; /* rehashing not in progress if rehashidx == -1 */
- } dict;
图例:
特点:
跳跃表节点:
- typedef struct zskiplistNode {
-
- // 后退指针
- struct zskiplistNode *backward;
-
- // 分值
- double score;
-
- // 成员对象
- robj *obj;
-
- // 层
- struct zskiplistLevel {
-
- // 前进指针
- struct zskiplistNode *forward;
-
- // 跨度
- unsigned int span;
-
- } level[];
-
- } zskiplistNode;
跳跃表:
- typedef struct zskiplist {
-
- // 表头节点和表尾节点
- struct zskiplistNode *header, *tail;
-
- // 表中节点的数量
- unsigned long length;
-
- // 表中层数最大的节点的层数
- int level;
-
- } zskiplist;
图例:
层,也就是level[]
字段,层的数量越多,访问节点速度越快。(因为它相当于是索引,层数越多,它索引就越细,就能很快找到索引值)
层中有一个forward
字段,用于从表头向表尾方向访问。
用于记录两个节点之间的距离
用于从表尾向表头方向访问。
Reids对整数存储专门作了优化,intset就是redis用于保存整数值的集合数据结构。当一个结合中只包含整数元素,redis就会用这个来存储。
intset数据结构:
- typedef struct intset {
-
- // 编码方式
- uint32_t encoding;
-
- // 集合包含的元素数量
- uint32_t length;
-
- // 保存元素的数组
- int8_t contents[];
-
- } intset;
ziplist是redis为了节约内存而开发的顺序型数据结构。它被用在列表键和哈希键中。一般用于小数据存储。
图例:
entry节点的布局
一个由ziplist组成的双向链表。但是一个quicklist可以有多个quicklist节点,它很像B树的存储方式。是在redis3.2版本中新加的数据结构,用在列表的底层实现。
表头结构:
- typedef struct quicklist {
- //指向头部(最左边)quicklist节点的指针
- quicklistNode *head;
-
- //指向尾部(最右边)quicklist节点的指针
- quicklistNode *tail;
-
- //ziplist中的entry节点计数器
- unsigned long count; /* total count of all entries in all ziplists */
-
- //quicklist的quicklistNode节点计数器
- unsigned int len; /* number of quicklistNodes */
-
- //保存ziplist的大小,配置文件设定,占16bits
- int fill : 16; /* fill factor for individual nodes */
-
- //保存压缩程度值,配置文件设定,占16bits,0表示不压缩
- unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
- } quicklist;
quicklist节点结构:
- typedef struct quicklistNode {
- struct quicklistNode *prev; //前驱节点指针
- struct quicklistNode *next; //后继节点指针
-
- //不设置压缩数据参数recompress时指向一个ziplist结构
- //设置压缩数据参数recompress指向quicklistLZF结构
- unsigned char *zl;
-
- //压缩列表ziplist的总长度
- unsigned int sz; /* ziplist size in bytes */
-
- //ziplist中包的节点数,占16 bits长度
- unsigned int count : 16; /* count of items in ziplist */
-
- //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
- unsigned int encoding : 2; /* RAW==1 or LZF==2 */
-
- //表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
- unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
-
- //标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
- //如果recompress为1,则等待被再次压缩
- unsigned int recompress : 1; /* was this node previous compressed? */
-
- //测试时使用
- unsigned int attempted_compress : 1; /* node can't compress; too small */
- //额外扩展位,占10bits长度
- unsigned int extra : 10; /* more bits to steal for future usage */
- } quicklistNode;
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。
底层结构
Redis 基于 Reactor 模式开发了自己的网络事件处理器 - 文件事件处理器(file event handler,后文简称为
FEH
),而该处理器又是单线程的,所以redis设计为单线程模型。 - 采用I/O多路复用同时监听多个socket,根据socket当前执行的事件来为 socket 选择对应的事件处理器。 - 当被监听的socket准备好执行accept
、read
、write
、close
等操作时,和操作对应的文件事件就会产生,这时FEH就会调用socket之前关联好的事件处理器来处理对应事件。所以虽然FEH是单线程运行,但通过I/O多路复用监听多个socket,不仅实现高性能的网络通信模型,又能和 Redis 服务器中其它同样单线程运行的模块交互,保证了Redis内部单线程模型的简洁设计。
文件事件处理器的结构包含 4 个部分:
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用 程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
来看客户端与 redis 的一次通信过程:
客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
这样便完成了一次通信。
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
为啥Redis单线程模型也能效率这么高?
1)纯内存操作
Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。
2)核心是基于非阻塞的IO多路复用机制
有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。
redis 需要处理多个 IO 请求,同时把每个请求的结果返回给客户端。由于 redis 是单线程模型,同一时间只能处理一个 IO 事件,于是 redis 需要在合适的时间暂停对某个 IO 事件的处理,转而去处理另一个 IO 事件,这就需要用到IO多路复用技术了, 就好比一个管理者,能够管理个socket的IO事件,当选择了哪个socket,就处理哪个socket上的 IO 事件,其他 IO 事件就暂停处理了。
3)单线程反而避免了多线程的频繁上下文切换带来的性能问题。
参考资料:《Redis设计与实现》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。