当前位置:   article > 正文

Redis学习笔记(四)底层数据结构及线程模型_redis底层线程

redis底层线程

目录

底层数据结构

简单动态字符串(SDS)

链表

字典

跳跃表

层(level[])

前进指针(forward)

跨度(span)

后退指针(backward)

整数集合

压缩列表

快速列表

对象

总结

线程模型


底层数据结构

简单动态字符串(SDS)

结构源码:

  1. struct sdshdr{
  2. //记录buf数组中已使用字节的数量
  3. //等于 SDS 保存字符串的长度
  4. int len;
  5. //记录 buf 数组中未使用字节的数量
  6. int free;
  7. //字节数组,用于保存字符串
  8. char buf[];
  9. }

图例:

SDS与C字符串对比

链表

ListNode节点数据结构:

  1. typedef struct listNode{
  2. //前置节点
  3. struct listNode *prev;
  4. //后置节点
  5. struct listNode *next;
  6. //节点的值
  7. void *value;
  8. }listNode

链表数据结构:

  1. typedef struct list{
  2. //表头节点
  3. listNode *head;
  4. //表尾节点
  5. listNode *tail;
  6. //链表所包含的节点数量
  7. unsigned long len;
  8. //节点值复制函数
  9. void (*free) (void *ptr);
  10. //节点值释放函数
  11. void (*free) (void *ptr);
  12. //节点值对比函数
  13. int (*match) (void *ptr,void *key);
  14. }list;

特点:

  1. 可以直接获得头、尾节点。
  2. 常数时间复杂度得到链表长度。
  3. 是双向链表。

字典

哈希表:

  1. typedef struct dictht {
  2. // 哈希表数组
  3. dictEntry **table;
  4. // 哈希表大小
  5. unsigned long size;
  6. // 哈希表大小掩码,用于计算索引值
  7. // 总是等于 size - 1
  8. unsigned long sizemask;
  9. // 该哈希表已有节点的数量
  10. unsigned long used;
  11. } dictht;

Hash表节点:

  1. typedef struct dictEntry {
  2. //
  3. void *key;
  4. //
  5. union {
  6. void *val;
  7. uint64_t u64;
  8. int64_t s64;
  9. } v;
  10. // 指向下个哈希表节点,形成链表
  11. struct dictEntry *next; // 单链表结构
  12. } dictEntry;

字典:

  1. typedef struct dict {
  2. // 类型特定函数
  3. dictType *type;
  4. // 私有数据
  5. void *privdata;
  6. // 哈希表
  7. dictht ht[2];
  8. // rehash 索引
  9. // 当 rehash 不在进行时,值为 -1
  10. int rehashidx; /* rehashing not in progress if rehashidx == -1 */
  11. } dict;

图例:

特点:

  1. Reids的Hash采用链地址法来处理冲突,然后它没有使用红黑树优化。
  2. 哈希表节点采用单链表结构。
  3. rehash优化。

跳跃表

跳跃表节点:

  1. typedef struct zskiplistNode {
  2. // 后退指针
  3. struct zskiplistNode *backward;
  4. // 分值
  5. double score;
  6. // 成员对象
  7. robj *obj;
  8. //
  9. struct zskiplistLevel {
  10. // 前进指针
  11. struct zskiplistNode *forward;
  12. // 跨度
  13. unsigned int span;
  14. } level[];
  15. } zskiplistNode;

跳跃表:

  1. typedef struct zskiplist {
  2. // 表头节点和表尾节点
  3. struct zskiplistNode *header, *tail;
  4. // 表中节点的数量
  5. unsigned long length;
  6. // 表中层数最大的节点的层数
  7. int level;
  8. } zskiplist;

图例:

层(level[])

层,也就是level[]字段,层的数量越多,访问节点速度越快。(因为它相当于是索引,层数越多,它索引就越细,就能很快找到索引值)

前进指针(forward)

层中有一个forward字段,用于从表头向表尾方向访问。

跨度(span)

用于记录两个节点之间的距离

后退指针(backward)

用于从表尾向表头方向访问。

整数集合

Reids对整数存储专门作了优化,intset就是redis用于保存整数值的集合数据结构。当一个结合中只包含整数元素,redis就会用这个来存储。

intset数据结构:

  1. typedef struct intset {
  2. // 编码方式
  3. uint32_t encoding;
  4. // 集合包含的元素数量
  5. uint32_t length;
  6. // 保存元素的数组
  7. int8_t contents[];
  8. } intset;
  • 如果 encoding 属性的值为 INTSET_ENC_INT16 , 那么 contents 就是一个 int16_t 类型的数组, 数组里的每个项都是一个 int16_t 类型的整数值 (最小值为 -32,768 ,最大值为 32,767 )。
  • 如果 encoding 属性的值为 INTSET_ENC_INT32 , 那么 contents 就是一个 int32_t 类型的数组, 数组里的每个项都是一个 int32_t 类型的整数值 (最小值为 -2,147,483,648 ,最大值为 2,147,483,647 )。
  • 如果 encoding 属性的值为 INTSET_ENC_INT64 , 那么 contents 就是一个 int64_t 类型的数组, 数组里的每个项都是一个 int64_t 类型的整数值 (最小值为 -9,223,372,036,854,775,808 ,最大值为 9,223,372,036,854,775,807 )。

压缩列表

ziplist是redis为了节约内存而开发的顺序型数据结构。它被用在列表键和哈希键中。一般用于小数据存储。

图例:

entry节点的布局

快速列表

一个由ziplist组成的双向链表。但是一个quicklist可以有多个quicklist节点,它很像B树的存储方式。是在redis3.2版本中新加的数据结构,用在列表的底层实现。

表头结构:

  1. typedef struct quicklist {
  2. //指向头部(最左边)quicklist节点的指针
  3. quicklistNode *head;
  4. //指向尾部(最右边)quicklist节点的指针
  5. quicklistNode *tail;
  6. //ziplist中的entry节点计数器
  7. unsigned long count; /* total count of all entries in all ziplists */
  8. //quicklist的quicklistNode节点计数器
  9. unsigned int len; /* number of quicklistNodes */
  10. //保存ziplist的大小,配置文件设定,占16bits
  11. int fill : 16; /* fill factor for individual nodes */
  12. //保存压缩程度值,配置文件设定,占16bits,0表示不压缩
  13. unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
  14. } quicklist;

quicklist节点结构:

  1. typedef struct quicklistNode {
  2. struct quicklistNode *prev; //前驱节点指针
  3. struct quicklistNode *next; //后继节点指针
  4. //不设置压缩数据参数recompress时指向一个ziplist结构
  5. //设置压缩数据参数recompress指向quicklistLZF结构
  6. unsigned char *zl;
  7. //压缩列表ziplist的总长度
  8. unsigned int sz; /* ziplist size in bytes */
  9. //ziplist中包的节点数,占16 bits长度
  10. unsigned int count : 16; /* count of items in ziplist */
  11. //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
  12. unsigned int encoding : 2; /* RAW==1 or LZF==2 */
  13. //表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
  14. unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
  15. //标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
  16. //如果recompress为1,则等待被再次压缩
  17. unsigned int recompress : 1; /* was this node previous compressed? */
  18. //测试时使用
  19. unsigned int attempted_compress : 1; /* node can't compress; too small */
  20. //额外扩展位,占10bits长度
  21. unsigned int extra : 10; /* more bits to steal for future usage */
  22. } quicklistNode;

对象

Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。

底层结构

总结

线程模型

Redis 基于 Reactor 模式开发了自己的网络事件处理器 - 文件事件处理器(file event handler,后文简称为 FEH),而该处理器又是单线程的,所以redis设计为单线程模型。 - 采用I/O多路复用同时监听多个socket,根据socket当前执行的事件来为 socket 选择对应的事件处理器。 - 当被监听的socket准备好执行acceptreadwriteclose等操作时,和操作对应的文件事件就会产生,这时FEH就会调用socket之前关联好的事件处理器来处理对应事件。

所以虽然FEH是单线程运行,但通过I/O多路复用监听多个socket,不仅实现高性能的网络通信模型,又能和 Redis 服务器中其它同样单线程运行的模块交互,保证了Redis内部单线程模型的简洁设计。

文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(包括:连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用 程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

来看客户端与 redis 的一次通信过程:

  1. 客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。

  2. 假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。

  3. 如果此时客户端准备好接收返回结果了,那么 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 适用于那些需要快速执行的场景。

 

参考资料:《Redis设计与实现》

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/157340
推荐阅读
相关标签
  

闽ICP备14008679号