redis-数据结构、io

1.redis 数据结构

结构图–Redis底层数据结构(图文详解)
redis核心篇(一)-底层数据结构

1.1 字典 kv是什么

每个键值对都会是一个dictEntry

·set hello word为例,因为Redis是KV键值对的数据库,每个键值对都会有一个dictEntry(源码位置:dict.h)

2

3
2
3

简单描述 加载原理

server 启动,加载redisdb进内存形成数据库,形成数据库后,马上读取dict 字典。
形成dict 字典后,读取dictht ht[2] 哈希表 ,里面有dictentry 数组, 数组里面每个都是dictentry对象
3

1.1.2 redisObject 作用

为了便于操作,redis 采用redisobject结构体来统一五种不同的数据类型。这样可以方便在函数之间传递。
2

1.2 redis 数据结构

redis6

  • String
    • SDS (int raw embstr)
  • set
    • intset + hashtable(dict)
  • hash
    • hahshtable dict + ziplist
  • list
    • 3.2 后quicklist、3.2前 ziplist(<=512 <=64Byte) 超过 linkedlist
  • sortedset
    • skiplist 、 ht; ziplist

2. 5大底层

2.1 sds

int
embstr 小于44字节
raw

C 字符串sds
获取字符串长度的复杂度为 O(N) 。获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。修改字符串长度 N 次最多需要执行 N 次内存重分配。 内存预先分配
二进制不安全 只能存储文本字符串二进制安全 可以保存文本或者二进制数据。
非二进制安全 指的是在 字符串内容中 存在 结束标识符,那么读取到一半就以为结束了。
  • 空间分配
    • 新的字符串小于1m,新空间为,新的字符串长度*2+1
    • 新的字符串大于1m,新空间为 ,新的字符串长度+1m+1; 称为内存预分配
  • 空间释放
    • SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

2


//lsb 代表有效位的意思,
//__attribute__ ((__packed__)) 代表structure 采用手动对齐的方式。
struct __attribute__ ((__packed__)) sdshdr8 {
    //buf 已经使用的长度
    uint8_t len; /* used */
    //buf 分配的长度,等于buf[]的总长度-1,因为buf有包括一个/0的结束符
    uint8_t alloc;
     /* excluding the header and null terminator */
    //只有3位有效位,因为类型的表示就是0到4,所有这个8位的flags
    // 有5位没有被用到
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    //实际的字符串存在这里
    char buf[];

Redis中字符串的实现,SDS有多种结构(sds.h):
sdshdr5、(2^5=32byte)
sdshdr8、(2 ^ 8=256byte)
sdshdr16、(2 ^ 16=65536byte=64KB)
sdshdr32、 (2 ^ 32byte=4GB)
sdshdr64,2的64次方byte=17179869184G用于存储不同的长度的字符串。

len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串。

alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。

buf 表示字符串数组,真存数据的

2.2 string

1 intLong类型整数时,RedisObject中的·ptr指针直接赋值为整数数据·,·不再额外的指针再指向整数了·,节省了指针的空间开销。
2 embstr当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片
3 raw当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr 结构

2

2.2.1 没有超过44b,也是raw

3

2.3 intset 唯一、有序

小总结:

Intset可以看做是特殊的整数数组,具备一些特点:

  • Redis会确保Intset中的元素唯一、有序
  • 具备类型升级机制(倒序拷贝数组元素),可以节省内存空间
  • 底层采用二分查找方式来查询
    3
    encoding 决定 contents 数组内 数字的范围
    升序 保存元素
    2

现在,数组中每个数字都在int16_t的范围内,因此采用的编码方式是INTSET_ENC_INT16,每部分占用的字节大小为:

  • encoding:4字节
  • length:4字节
  • contents:2字节 * 3 = 6字节 (每个长度占用2个字节,方便inset 基于数组角标,快速访问数组的元素)
    contents 地址是 数组的 第一个元素的位置,

inset 升级
2
倒序拷贝原数组的元素
数字20 拷贝到 8-12
10 拷贝到 4-8
5 拷贝到 1-4
2
最后添加 50000 到数组末尾12 -16
2

我们向该其中添加一个数字:50000,这个数字超出了int16_t的范围,intset会自动升级编码方式到合适的大小。
以当前案例来说流程如下:

  • 升级编码为INTSET_ENC_INT32, 每个整数占4字节,并按照新的编码方式及元素个数扩容数组
  • 倒序依次将数组中的元素拷贝到扩容后的正确位置
  • 将待添加的元素放入数组末尾
  • 最后,将inset的encoding属性改为INTSET_ENC_INT32,将length属性改为4

2.4 字典 dict

dict 三部分组成 dict、dicthashtable(哈希表 数组) 、dictentry

小总结:

Dict的结构:

  • 类似java的HashTable,底层是数组加链表来解决哈希冲突
  • Dict包含两个哈希表,ht[0]平常用,ht[1]用来rehash

Dict的伸缩:

  • 当LoadFactor大于5或者LoadFactor大于1并且没有子进程任务时,Dict扩容
  • 当LoadFactor小于0.1时,Dict收缩
  • 扩容大小为第一个大于等于used + 1的2^n
  • 收缩大小为第一个大于等于used 的2^n
  • Dict采用渐进式rehash,每次访问Dict时执行一次rehash
  • rehash时ht[0]只减不增,新增操作只在ht[1]执行,其它操作在两个哈希表

字典dict 类型
3
2
3

3

2.4.1 dict的 扩缩容 的条件

dict扩容条件
元素过多,导致hash冲突,链表太长,查询效率降低。
used sieze 在dictht 结构体中
dict 在每次新增键值对 k v的时候 ,会判断负载因子(LoadFactor=used/size) ,满足以下下条件触发扩容:

  • 负载因子大于等于1,并且后台没有执行BGSAVE或者BGREWRITE等命令。
  • 负载因子>5 也需要扩容。
    扩容大小为 used+1 的2^n次
    2
    dict 收缩的条件
    每次删除元素的时候,会检查负载因子,当LoadFactor<0.1时候,会做hashtable 收缩。
    但是需要大于 hashtable 初始化大小 4 ,收缩小于4,那么重置为hashtable 最小值 4.

2.4.2 dict 的扩容 过程

dict 的rehash的过程 渐进式hash
rehash:因为进行 扩容或者缩容, 导致hashtable 的size 和sizemask变化,而key 的查询根据sizemask。·需要对每一个 旧的hashtable中的key进行重新计算索引,插入新的hashtable,这个过程称为rehash。

  1. 计算新的hash表的 realsize,取决于当前扩容还是缩容

    • 扩容,realsize 是 第一个大于等于used+1的 2^n的数。
    • 缩容 realsize 是第一个大于等于 used的 2^n 的数 但是不得小于4
  2. 根据realsize大小,申请新的hash表,赋值给dict.ht[1]

  3. 设置 dict.rehashindex=0,表示开始rehash
    4.讲dict.ht[0] 中的每个entry 转移到 dict.ht[1]

  4. ·每次新增、查询、修改、添加 元素,都检查一下 dict.rehashindex 是否大于 -1,如果大于-1,就将dict.ht[0].table[rehashindex] entry链表 rehash 到dict.ht[1] 中,并且rehashindex++,最后知道dict.ht[0]的元素都迁移到dict.ht[1]中

  5. 讲dict.ht[1] 赋值给 dict.ht[0],dict.ht[1]为空,释放原来旧的dict.ht[0]的内存

  6. ·讲rehashindex 置为-1, 表示rehash结束。

  7. 修改、查询、删除的操作,都会在dict.ht[0]和dict.ht[1] 中执行。新增 直接添加到dict.ht[1]中

3

rehash 后
3

2.5 ziplist

dict 内部使用大量指针(8Byte),内存不连续,容易产生内存碎片。内存浪费。
引入ziplist
ziplist 是特殊的"双端链表",是特殊编码的连续内存块组成。任意一端可以进行压入/弹出。时间复杂度为0(1);

小总结:

ZipList特性:

  • 压缩列表的可以看做一种连续内存空间的"双向链表"
  • 列表的节点之间不是通过指针连接,而是记录上一节点和本节点长度来寻址,内存占用较低
  • 如果列表数据过多,导致链表过长,可能影响查询性能
  • 增或删较大数据时有可能发生连续更新问题

3

2.5.1 ziplist entry

2

  • previous_entry_length:·前一节点的长度,占1个或5个字节。

    • 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
    • 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
  • encoding:编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节

  • contents:负责保存节点的数据,可以是字符串或整数

  • 正向遍历

    • previous 块字节长度 + encoding 块字节长度 + encoding 中获取 content字节长度 就是 当前entry 的总字节长度。
    • 所以只需要 当前entry首地址 + entry 总长度就可以获得下一个entry 首地址
  • 反向遍历

    • 当前entry 首地址,- previous 中 上个entry 占用字节数,相减就可以得到。

ZipList中所有·存储长度的数值均采用``小端字节序,即低位字节在前,高位字节在后。例如:数值0x1234,采用小端字节序后实际存储值为:0x3412

2.5.2 encoding

Encoding编码

ZipListEntry中的encoding编码分为字符串和整数两种:
字符串:如果encoding是以“00”、“01”或者“10”开头,则证明content是字符串

编码编码长度字符串大小
|00pppppp|1 bytes<= 63 bytes
|01pppppp|qqqqqqqq|2 bytes<= 16383 bytes
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|5 bytes<= 4294967295 bytes

例如,我们要保存字符串:“ab”和 “bc”

3

3

ZipListEntry中的encoding编码分为字符串和整数两种:

  • 整数:如果encoding是以“11”开始,则证明content是整数,且encoding固定只占用1个字节

2.5.3 ziplist 连锁更新问题

2.6 quicklist

总结:

QuickList的特点:

  • 是一个节点为ZipList的双端链表
  • 节点采用ZipList解决了传统链表的内存占用问题 (传统链表采用指针占用内存)
  • 控制了ZipList大小,解决连续内存空间申请效率问题 ()
  • 中间节点可以压缩,进一步节省了内存

一句话: qiucklist 采用linkedlist+ziplist,可以双端访问,内存占用较低相对于链表,包含多个ziplist,存储上限高。

3

3

ziplist 连续内存空间太大,不在好分配。
引入quicklist

问题1:ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请内存效率很低。怎么办?

​ 答:为了缓解这个问题,我们必须限制ZipList的长度和entry大小。

问题2:但是我们要存储大量数据,超出了ZipList最佳的上限该怎么办?

​ 答:我们可以创建多个ZipList来分片存储数据。

问题3:数据拆分后比较分散,不方便管理和查找,这多个ZipList如何建立联系?

​ 答:Redis在3.2版本引入了新的数据结构QuickList它是一个双端链表,只不过链表中的每个节点都是一个ZipList

3

2.6.1 限制quicklist 中每个ziplist中的 entry 大小

list-max-ziplist-size

为了避免QuickList中的每个ZipList中entry过多,Redis提供了一个配置项:list-max-ziplist-size来限制。
如果值为正,则代表ZipList的允许的entry个数的最大值
如果值为负,则代表ZipList的最大内存大小,分5种情况:

  • -1:每个ZipList的内存占用不能超过4kb
  • -2:每个ZipList的内存占用不能超过8kb 默认值
  • -3:每个ZipList的内存占用不能超过16kb
  • -4:每个ZipList的内存占用不能超过32kb
  • -5:每个ZipList的内存占用不能超过64kb

2.6.2 quicklist 对ziplist 压缩

2
3

3

2.7 skiplist 升序

可以从中间随机查询

Skiplist 特点

  • 元素升序排列存储
  • 节点可以包含多个指针,跨度不同

总结

  • 跳表是一个双向链表,每个节点有 score(排序查找用) 和ele值 (存储的值)
  • 节点根据score 升序排序,score一样,采用ele 排序
  • 每个节点 包含多个层,level[],最多32层
  • 不同层,指向下一个节点的跨度不同。

3

3

3 redis 内部结构

数据类型编码方式
OBJ_STRINGint、embstr、raw
OBJ_LISTLinkedList和ZipList(3.2以前)、QuickList(3.2以后)
OBJ_SETintset、HT
OBJ_ZSETZipList、HT、SkipList
OBJ_HASHZipList、HT

3.1 hash

hash-max-ziplist-entries: 默认 512个field 使用压缩列表保存时哈希集合中的最大元素个数。

hash-max-ziplist-value:默认64byte 使用压缩列表保存时哈希集合中单个元素的最大长度。

当数据量较大时,ZIPLIST编码会转为HT编码,也就是Dict,触发条件有两个:

  • ZipList中的元素数量超过了hash-max-ziplist-entries(默认512)
  • ZipList中的任意entry大小超过了hash-max-ziplist-value(默认64字节)
    x

3.2 set

底层采用 intset 、ht 的数据结构

为了查询效率和唯一性,set采用HT编码(Dict)。Dict中的key用来存储元素,value统一为null。

当存储的所有数据都是整数,并且元素数量不超过set-max-intset-entries(512)时,Set会采用IntSet编码,以节省内存·

intset集合元素超出set-max-intset-entries(512)时候,采用ht.
2
3

3.3 zset

  • 可以根据member 排序
  • member必须唯一
  • 可以根据member 查询分数

根据键值 存储 、唯一 、排序 ( intset 升序排列,skiplist 根据score 升序)

  • skiplist 可以排序,并且可以同时 存储score 和 ele 值(member) 满足键值存储,可排序
  • dict 键值存储,根据key 找value 满足 键值存储,唯一

所以 zset 采用 skiplist + dict
3

3

3.3.1 zset ziplist

当元素不多的时候,dict和skiplist的优势不明显,反而浪费内存。因此zset 还会采用ziplist的结构来节省内存。不过需要满足条件。

  • 元素数量小于zset-max-ziplist-entries 128
  • 每个元素的都小于 zset-max-ziplist-value 64
    3
    添加元素的时候,会判断 数量 <=128 && entry大小 <64 采用ziplist
    否则 转为 dict+skiplist

·ziplist本身没有排序功能,而且没有键值对的概念,因此需要有zset通过编码实现:

  • ZipList是连续内存,因此score和element是紧挨在一起的两个entry, ··element在前,score在后
  • score越小越接近队首,score越大越接近队尾,按照score值升序排列
    3

3.4 string

Redis底层-String类型
C语言用字符数组存储的问题:
1、获取长度需要遍历整个数组,O(n)复杂度
2、字符串append 需要扩容
3、无法存储图片(二进制数据),因为会对空格字符解释成字符串的结尾符

避免上述问题,C语言中抽象了一个SDS(Simple Dynamic String)的数据结构
1、lines 表示已经存储的长度,查询长度时间降为O(1);
2、空间预分配,free记录可用的长度大小;预分配(扩容)策略:未超过1MB,长度扩容成原来的两倍;超过1MB,长度扩容1MB
3、额外实现了添加字符的API,不是沿用C语言的String API,不会对空格字符解释成字符串的结尾符

3.5 list

  • LinkedList :普通链表,可以从双端访问,内存占用较高,内存碎片较多
  • ZipList :压缩列表,可以从双端访问,内存占用低,存储上限低
  • QuickList:LinkedList + ZipList,可以从双端访问,内存占用较低,包含多个ZipList,存储上限高

在·3.2版本之前,Redis采用ZipList和LinkedList来实现List,`当元素数量小于512并且元素大小小于64字节时采用ZipList编码,超过则采用LinkedList编码。

< 3.2 ziplist linkedlist,
元素大小 小于 512个,并且 元素大小 小于64字节 采用ziplist
否则采用 linkedlist
> 3.2版本 采用quicklist
3

4. redis 网络io模型

4.1 用户空间、内核空间

Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:

  • 写数据时,要把用户缓冲数据拷贝到内核缓冲区然后写入设备

  • 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
    3

4.2 阻塞io

3

阻塞io 两个阶段都必须阻塞等待。
2

4.3 非阻塞io

recvfrom 操作 立即返回结果而不是阻塞用户进程

2
可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转CPU使用率暴增。

4.4 io多路复用

就是一种机制,通过一个进程可以监听多个文件描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作。
IO多路复用是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听FD的方式、通知的方式又有多种实现,常见的有:

  • select
  • poll
  • epoll

其中select和pool相当于是当被监听的数据准备好之后,他会把你监听的FD整个数据都发给你,你需要到整个FD中去找,哪些是处理好了的,需要通过遍历的方式,所以性能也并不是那么好

而epoll,则相当于内核准备好了之后,他会把准备好的数据,直接发给你,咱们就省去了遍历的动作。
3
阻塞io 两个步骤也是阻塞的,io多路复用两个步骤也是阻塞的有什么差别??

阻塞io 调用recvfrom产生的阻塞,但是recvfrom 只能监听一个fd。
select 模式,一次性监听多个fd,只要有一个就绪,就可以处理。

4.4.1 select

// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;
/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
    // fds_bits是long类型数组,长度为 1024/32 = 32
    // 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪 ,32*32=1024
    // 按比特位 存 fd 1024个
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];  // 数组长度32  每个元素32位
    // ...
} fd_set;
// select函数,用于监听fd_set,也就是多个fd的集合 
// 返回值,有几个就绪的fd
int select(
    int nfds, // 要监视的fd_set的最大fd + 1 
    fd_set *readfds, // 要监听读事件的fd集合   请求的fd
    fd_set *writefds,// 要监听写事件的fd集合   响应的fd
    fd_set *exceptfds, // // 要监听异常事件的fd集合
    // 超时时间,null-用不超时;0-不阻塞等待;大于0-固定等待时间
    struct timeval *timeout
);

3

过程

比如要监听的数据,是1,2,5三个数据,此时会执行select函数,然后将整个fd发给内核态,内核态会去遍历用户态传递过来的数据,如果发现这里边都数据都没有就绪,就休眠,直到有数据准备好时,就会被唤醒,唤醒之后,再次遍历一遍,看看谁准备好了,然后再将处理掉没有准备好的数据,最后再将这个FD集合写回到用户态中去,此时用户态就知道了,奥,有人准备好了,但是对于用户态而言,并不知道谁处理好了,所以用户态也需要去进行遍历,然后找到对应准备好数据的节点,再去发起读请求,我们会发现,这种模式下他虽然比阻塞IO和非阻塞IO好,但是依然有些麻烦的事情, 比如说频繁的传递fd集合,频繁的去遍历FD等问题

select 问题
  1. 需要将整个fd_set 从用户态拷贝到内核态,select 结束,还要再次拷贝回用户空间。(发生两次数据拷贝)
  2. select 无法知道那个fd就绪,需要遍历整个fd。
  3. fd_set 监听的fd数量不能超过1024个。fd 是长度位1024的数组

4.4.2 poll

解决了fd 1024上下问题。其他两个问题没有解决。

poll模式对select模式做了简单改进,但性能提升不明显,部分关键代码如下:

IO流程:

  • 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
  • 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
  • 内核遍历fd,判断是否就绪
  • 数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
  • 用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd

与select对比:

  • select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
  • 监听FD越多,每次遍历消耗时间也越久·,性能反而会下降
    3

4.4.3 epoll

提供了3个函数

  1. epoll_create(int size)
    3
  2. epoll_ctl(int epfd ,int op, int fd ,struct epoll_event *event)
    2
    callback触发时,添加到rd list 中
  3. epoll_wait( int epfd,struct epoll_event *events,int maxevents ,int timeout)
    3
    2
    第一个是:eventpoll的函数,他内部包含两个东西

一个是:

1、红黑树-> 记录的事要监听的FD

2、一个是链表-> 一个链表,记录的是就绪的FD

紧接着调用epoll_ctl操作将要监听的数据添加到红黑树上去并且给每个fd设置一个监听函数,这个函数会在fd数据就绪时触发,就是准备好了,现在就把fd把数据添加到list_head中去

3、调用epoll_wait函数

就去等待,·在用户态创建一个空的events数组当就绪之后,我们的回调函数会把数据添加到list_head中去,当调用这个函数的时候,会去检查list_head,当然这个过程需要参考配置的等待时间,可以等一定时间,也可以一直等, 如果在此过程中,检查到了list_head中有数据会将数据添加到链表中此时将数据放入到events数组中,并且返回对应的操作的数量,用户态的此时收到响应后,从events中拿到对应准备好的数据的节点,再去调用方法去拿数据。

4.4.5 3种总结

小总结:

select模式存在的三个问题:

  • 能监听的FD最大不超过1024
  • 每次select都需要把所有要监听的FD都拷贝到内核空间
  • 每次都要遍历所有FD来判断就绪状态

poll模式的问题:

  • poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降

epoll模式中如何解决这些问题的?

  • 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
  • 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
  • 1利用ep_poll_callback机制来监听FD状态无需遍历所有FD,因此性能不会随监听的FD数量增多而下降

4.5 epoll 触发方式

边缘触发 减少epoll_wait() 调用次数,提高程序效率
水平触发:

  • 当fd 有数据可读时候,会重复通知,知道数据处理完成为止。epoll 默认模式/

边缘触发:

  • 当fd数据可读时,只会通知一次,不管数据是否读取完成。

举个栗子:

  1. 假设一个客户端socket对应的FD已经注册到了epoll实例中
  2. 客户端socket发送了2kb的数据
  3. 服务端调用epoll_wait,得到通知说FD就绪
  4. 服务端从FD读取了1kb数据
  5. 回到步骤3(再次调用epoll_wait,形成循环)

结果:
如果我们采用LT模式,因为FD中仍有1kb数据,则第⑤步依然会返回结果,并且得到通知

如果我们采用ET模式,因为第③步已经消费了FD可读事件,第⑤步FD状态没有变化,因此epoll_wait不会返回,数据无法读取,客户端响应超时。

3

3

et 模式,在读取的时候,在一次通知中,为了一次读取完所有数据。可以使用非阻塞io,没数据了就返回。阻塞io没数据回阻塞。

5. redis 多线程处理io

5.1 redis单线程网络模型

  1. 纯内存操作,性能快。
  2. 引入多线程会导致过多的上下文切换,带来不必要的开销。
  3. 多线程会面临线程安全的问题。
  • redis 通过io 多路复用提高网络性能,并且支持各种不同的实现。将实现进行封装,提高统一的高性能事件。API库 AE
    3

根据ae.c文件,对系统环境进行判断,声明一些变量,
have epoll
#include ae_epoll.c // 支持eopoll 那么引入epoll文件
3
3

5.2 redis 单线程网络模型代码

main 函数

  1. 初始化服务 initServer();
    1.1 内部调用apiCreate()。类似 创建epoll_create();
    1.2 监听TCP端口, 创建serverSocket, 得到ssfd
    1.3 得到ssfd,监听ssfd, 注册ssfd 到eventLoop 上 也就是epoll 上,并且配置好serversocket 对应的处理器。acceptTcpHandler事件处理 回调。
    1.4 在调用wait 之前,做一些准备工作 注册 before 前置处理器。
  2. 开始监听事件循环,aeMain()。

3
before sleep()
遍历队列中的client,监听客户端 fd 的写事件,绑定写处理器 SendReplyToclient
定义迭代器,在sleep 前,从代谢客户端队列中 取出客户端,
setWriteHandler
3. 监听客户端 写的fd
4. 设置写处理器

3

·单线程模型·
a

5.2 多线程模型

Redis 6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。

命令 解析 、解析请求当中的命令。
写响应结果
这两处采用多线程。

4

6 过期删除策略

** 周期删除模式**
SLOW模式规则:

  • 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。
  • 执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms
    步骤:
  • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期(过期就删除,并统计全局过期的key,和所有的key)
  • 如果没达到时间上限(25ms)并且过期key(全局中,过期的key占总的key)比例大于10%,再进行一次抽样,否则结束

FAST模式规则(过期key比例小于10%不执行 ):

  • 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
  • 执行清理耗时不超过1ms
    步骤
  • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
    如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束

7 io 模型

7.1 bio

bio 不足:

  1. 进程执行recvfrom()阻塞等待数据,等待内核的数据就绪,发生进行切换,让出cpu 执行时间。
  2. 当客户端数据被复制到 服务端 内核态,socket 接受队列的时候,服务端进程切换,被唤醒 切换到运行队列。,但是 还是阻塞,将数据从内核态拷贝到用户态缓冲区解除阻塞。
  3. 2次进程切换,2次阻塞。
    3

7.2 nio

  1. 当客户端数据发送到数据接受队列的时候,会阻塞等待读数据,发时进程切换,让出cpu 执行。
  2. 当recvfrom 把数据从内核态拷贝到 用户态缓冲区,进程a会唤醒。发生进程切换,加入到运行队列。
    发生2次进程切换。 将bio 的两次阻塞 变成1 次阻塞。
    accept() 方法 不会阻塞。 但是会while(true) 一直占用cpu。

3

7.3 select

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis是一个开源的内存数据库,使用C语言编写。它支持多种数据结构,如字符串、哈希表、列表、集合等。Redis-6.2.6是Redis的一个版本,而"windows"则代表它的部署环境是Windows操作系统。 相对于Linux环境,Windows上的Redis部署略有不同。首先,在Windows上安装Redis需要下载Redis的Windows版本,并解压到指定目录。接下来,使用命令行工具进入Redis所在目录,并通过运行redis-server.exe启动Redis服务器。 在Windows上,Redis默认以后台运行的方式启动,可以通过运行redis-cli.exe来连接Redis服务器并执行命令。可以使用redis-cli.exe的各种命令来操作Redis数据库,例如设置键值对、获取键值对、删除键值对等。 Redis-6.2.6在Windows上的功能与其他平台上的版本基本相同,但在Windows平台上使用Redis可能会面临一些限制。例如,由于Windows操作系统的限制,Redis-6.2.6在Windows上不能以集群模式启动,也无法使用Unix套接字来进行进程间通信。 此外,Redis-6.2.6在Windows上的性能可能会受到一些影响,因为Windows操作系统在处理网络请求和文件IO方面相对较慢。因此,在生产环境中,推荐将Redis部署在Linux等类Unix系统上,以获得更好的性能和稳定性。 总之,Redis-6.2.6可以在Windows上部署和使用,但需要注意一些限制和性能方面的考虑。 ### 回答2: Redis是一个开源的,内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。Redis 6.2.6是Redis最新的稳定版本,适用于Windows操作系统。在Windows上安装Redis需要进行一些步骤。 首先,我们需要在Redis的官方网站上下载Redis 6.2.6的Windows版本安装包,下载完成后解压缩到一个目录中。 接下来,进入Redis解压目录,在该目录下找到redis.windows.conf文件并打开,可以进行一些配置的修改,比如设置监听的端口号等。 然后,我们需要打开命令提示符或PowerShell窗口,进入Redis解压目录,并执行redis-server.exe redis.windows.conf命令,这将启动Redis服务器。 此时,我们可以通过执行redis-cli.exe命令进入Redis客户端,这样我们就可以与Redis进行交互了。 在Redis客户端中,我们可以使用一系列命令来操作Redis数据库,比如设置键值对、获取键值对、删除键值对等。 另外,如果需要使用Redis作为缓存或消息中间件,我们可以在应用程序中通过Redis的客户端库来连接和操作Redis数据库。 总结来说,Redis 6.2.6是Redis的最新稳定版本,适用于Windows操作系统。我们可以通过下载安装包并解压缩来安装Redis,在启动Redis服务器后,使用redis-cli命令进入Redis客户端,通过命令来与Redis进行交互和操作数据。 ### 回答3: Redis是一个开源的、基于内存的数据结构存储系统。Redis-6.2.6是Redis的一个版本,特指该版本适用于Windows操作系统。 相比于Linux系统,Windows环境下使用Redis有一些特殊的注意事项。首先,在Windows下使用Redis时,官方默认提供了基于MS Open Tech的Redis for Windows版本,这是官方维护的Windows版本。其次,需要注意的是Redis在Windows下的性能比Linux下稍差,主要是因为Windows下的文件系统性能与Linux下的不同导致的。 在Windows环境中安装Redis-6.2.6需要进行以下一些步骤。首先,从Redis官方网站下载Redis for Windows的可执行文件。然后,解压缩下载的压缩包并进入解压后的目录。接下来,运行redis-server.exe启动Redis服务,此时Redis服务器就开始监听默认的6379端口。可以执行redis-cli.exe来启动Redis命令行界面,进行操作和管理。此外,在Windows环境下,Redis可以作为后台服务运行,可以使用redis-server --service-install命令将Redis安装为Windows服务。 值得注意的是,Redis在Windows下默认是以单线程方式运行的,即每次只能处理一个请求,这会对性能造成影响。如果需要提高性能,可以使用集群部署多个Redis实例,并通过Redis Sentinel或Redis Cluster来管理这些实例。 总之,Redis-6.2.6适用于Windows操作系统,可以在Windows环境中使用Redis提供的功能和特性。需要注意的是,在Windows下使用Redis时需要特别留意性能方面的差异,并按照官方提供的方法正确安装和配置Redis服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值