0%

Redis原理篇

  • 原理篇
    • 数据结构
    • 网络模型
    • 通信协议
    • 内存策略

三、★原理篇

3.1 数据结构

3.1.1 动态字符串SDS

  • 我们都知道 Redis 中保存的 key 是字符串,value 往往是字符串或者字符串的集合,可见字符串是 Redis 中最常用的一种
    数据结构。不过Redis 并没有直接使用 C 语言中的字符串,而是构建了一种新的字符串结构,称为简单动态字符串(Simple Dynamic String),简称 SDS

    • 例如,当我们执行命令 SET name jack,那么 Redis 将在底层创建两个 SDS,其中一个是包含 “name” 的 SDS,另一个是包含 “jack” 的 SDS。
  • Redis 是 C 语言实现的,SDS 就是 C 语言中的结构体,源码如下:

    1
    2
    3
    4
    5
    6
    struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t Len; /*buf已保存的字符串字节数,不包含结束标示*/
    uint8_t alloc; /*buf申请的总的字节数,不包含结束标示*/
    unsigned char flags; /*不同SDS的头类型,用来控制SDS的头大小*/
    char buf[];
    }
    • SDS 之所以叫做动态字符串,是因为它具备动态扩容的能力。

3.1.2 IntSet

  • IntSet 是 Redis 中 set 集合的一种实现方式,基于整数数组来实现,并且具备长度可变、有序、唯一等特征。其结构如下:
IntSet类型自动升级
  • 总结:Intset 可以看做是特殊的整数数组,具备一些特点:

    1. Redis 会确保 Intset 中的元素唯一、有序
    2. 具备类型升级机制,可以节省内存空间。
    3. 底层采用二分查找方式来查询、插入。

    适用于数据量不多的情况,因为 contents 数组最多存放 2^8 = 256 个元素地址。

3.1.3 Dict

  • 我们知道 Redis 是一个键值型(Key-Value Pair)的数据库,我们可以根据键实现快速的增删改查。而键与值的映射关系正是通过 Dict 来实现的。

    Dict 由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)。

    • 当我们向 Dict 添加键值对时,Redis 首先根据 key 计算出 hash 值 h,然后利用 h & sizemask 来计算元素应该存储到数组中的哪个索引位置。例如,下面的例子中,首先存储 k1,然后存储 k2,k1 和 k2 都存储在数组索引为 1 的位置,所以就会形成一条链表,就像 Java 的 Hashtable 那样,而且在 Dict 中,新存进来的数据挂在链表首部

    • Dict 的基本结构示例图如下:

渐进式rehash
  • Dict 中的 HashTable 就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,则查询效率会大大降低,为此,Dict 在一定条件下会进行哈希表的扩容:

    • Dict 在每次新增键值对时都会检查负载因子(LoadFactor=used/size),满足以下两种情况时会触发哈希表扩容:
      • 哈希表的 LoadFactor >= 1,并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程。
      • 哈希表的 LoadFactor > 5。

    Dict 除了扩容以外,每次删除元素时,也会对负载因子做检查,当 LoadFactor<0.1 时,会做哈希表收缩。

  • 不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的 size 和 sizemask 变化,而 key 的查询与 sizemask 有关。因此必须对哈希表中的每一个 key 重新计算索引,插入新的哈希表,这个过程称为 rehash。过程是这样的:

    1. 计算新 hash 表的 realSize,值取决于当前要做的是扩容还是收缩:
      • 如果是扩容,则新 size 为第一个大于等于 dict.ht[0].used + 1 的 2^n。
      • 如果是收缩,则新 size 为第一个大于等于 dict.ht[0].used 的 2^n(不得小于4)。
    2. 按照新的 realSize 申请内存空间,创建 dictht,并赋值给 dict.ht[1]。
    3. 设置 dict.rehashidx = 0,标示开始 rehash。
    4. 将 dict.ht[0] 中的每一个 dictEntry 都 rehash 到 dict.ht[1]。
    5. 将 dict.ht[1] 赋值给 dict.ht[0],给 dict.ht[1] 初始化为空哈希表,释放原来的 dict.ht[0] 的内存。
  • Dict 的 rehash 并不是一次性完成的。试想一下,如果 Dict 中包含数百万的 entry,要在一次 rehash 完成,极有可能导致主线程阻塞。因此 Dict 的 rehash 是分多次、渐进式的完成,因此称为渐进式 rehash。流程如下:

    1. 计算新 hash 表的 realSize,值取决于当前要做的是扩容还是收缩:

      • 如果是扩容,则新 size 为第一个大于等于 dict.ht[0].used + 1 的 2^n。
      • 如果是收缩,则新 size 为第一个大于等于 dict.ht[0].used 的 2^n(不得小于4)。
    2. 按照新的 realSize 申请内存空间,创建 dictht,并赋值给 dict.ht[1]。

    3. 设置 dict.rehashidx = 0,标示开始 rehash。

    4. 每次执行新增、查询、修改、删除操作时,都检查一下 dict.rehashidx 是否大于 -1,如果是则将 dict.ht[0].table[rehashidx] 的 entry 链表 rehash 到 dict.ht[1],并且将 rehashidx++。直至 dict.ht[0] 的所有数据都 rehash 到 dict.ht[1]。 -> 渐进式rehash

    5. 将 dict.ht[1] 赋值给 dict.ht[0],给 dict.ht[1] 初始化为空哈希表,释放原来的 dict.ht[0] 的内存。

    6. 将 rehashidx 赋值为 -1,代表 rehash 结束。

      注意:在 rehash 过程中,新增操作,则直接写入 dict.ht[1],查询、修改和删除则会在 dict.ht[0] 和 dict.ht[1] 依次查找并执行。这样可以确保 dict.ht[0] 的数据只减不增,随着 rehash 最终为空。

3.1.4 ZipList

  • Dict 内部使用了大量的指针,用来指向分离的内存块,使得内存碎片化,同时指针自身也会占用一定的内存空间,所以 Dict 对内存资源的占用是较多的。

    • 为了尽可能的节省内存,设计了一种新的数据结构 ZipList
  • ZipList 是一种特殊的“双端链表”,由一系列特殊编码的连续内存块组成,可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为 O(1)。

  • ZipList 中的 entry 又由三部分组成:

ZipList的连锁更新问题

3.1.5 QuickList

  • QA:

    • ZipList 虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请内存效率很低。怎么办?
      • 为了缓解这个问题,我们必须限制 ZipList 的长度和 entry 大小。
    • 但是我们要存储大量数据,超出了 ZipList 最佳的上限该怎么办?
      • 我们可以创建多个 ZipList 来分片存储数据。
    • 数据拆分后比较分散,不方便管理和查找,这多个 ZipList 如何建立联系?
      • 通过链表的方式来联系。
  • Redis 在 3.2 版本引入了新的数据结构 QuickList,它是一个双端链表,只不过**链表中的每个节点都是一个 ZipList**。

3.1.6 SkipList

  • QuickList 需要从头结点或者尾结点开始查询,如果要查询中间结点,效率就会很低,而 SkipList 使得我们可以跳着找结点,从而提高性能。

  • SkipList(跳表)首先是链表,但与传统链表相比有几点差异:

    • 元素按照升序排列存储
    • 节点可能包含多个指针,指针跨度不同(可以跳着找)

3.1.7 RedisObject

  • 前面六种数据结构都是 Redis 数据结构的底层实现,**这六种数据结构最终会被封装为 RedisObject**。

  • Redis 中的任意数据类型的键和值都会被封装为一个 RedisObject,也叫做 Redis 对象,源码如下:

  • Redis 中常用的五种数据结构的底层数据结构如下:

★3.1.8 五种数据结构编码方式

3.1.8.1 String
3.1.8.2 List (QuickList)
3.1.8.3 Set (Dict、IntSet)
  • Set 既可能采用 HT 编码(Dict),也可能采用 IntSet 编码。

    • 只有当存储的所有数据都是整数,并且元素数量不超过指定的 set-max-intset-entries 时,才会使用 IntSet 编码

      • 如果此时向 IntSet 编码的 Set 中插入一个新元素,使得当前这个 Set 违背了 IntSet 编码的两个条件中的任意一个,那该 Set 就会由 IntSet 编码转换为 HT 编码(Dict)。
3.1.8.4 ★ZSet (SkipList+Dict、ZipList)
  • ZSet 就是 SortedSet

    • 由于 ZSet 需要兼顾使用 SkipListDict 的特性,所以将一份数据存成两份,虽然性能好,但是非常占用内存空间,当数据量较少时使用 SkipListDict 的优势不明显。

      • 因此 ZSet 还会采用 ZipList 结构来节省内存,不过需要同时满足以下两个条件:

        1. 元素数量小于 zset_max_ziplist_entries,默认值 128
        2. 每个元素都小于 zset_max_ziplist_value 字节,默认值 64
      • ZipList 本身没有排序功能,而且没有键值对的概念,因此需要通过代码逻辑来实现 ZSet 所需的功能:

        • ZipList 是连续内存,因此 score 和 element 是紧挨在一起的两个 entry,element 在前,score在后。
        • score 越小越接近队首,score 越大越接近队尾,按照 score 值升序排列。
  • Set 类似,ZSet 也存在从一种编码方式向另一种编码方式转换的可能性,即从 ZipList 转换为 SkipList+Dict

3.1.8.5 Hash (ZipList、Dict)
  • Hash 结构与 Redis 中的 ZSet 非常类似:

    • 都是键值存储
    • 都需求根据键获取值
    • 键必须唯一

    区别在于:

    • ZSet 的键是 member,值是 score;Hash 的键和值都是任意值。
    • ZSet 要根据 score 排序;**Hash 则无需排序**。

    因此,我们完全可以参考 ZSet 的编码,来设计 Hash 的编码,只需要把排序有关的 SkipList 去掉即可。

  • Hash 同样存在编码方式的转换问题,源码分析直接看视频 P159 吧。

3.2 网络模型

3.2.1 用户空间和内核空间

  • 任何 Linux 发行版,其系统内核都是 Linux,我们的应用都需要通过 Linux 内核与硬件交互。

    • 为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:

      进程的寻址空间会划分为两部分:内核空间用户空间

      • 用户空间只能执行受限的命令 (Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问。
      • 内核空间可以执行特权命令 (Ring0),调用一切系统资源。
    • Linux 系统为了提高 IO 效率,会在用户空间和内核空间都加入缓冲区:

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

    • 从图中可以看到,影响 IO 效率的主要因素有两个:

    • 等待数据从硬件读取到内核空间 wait for data

      • 等待数据从内核缓冲区拷贝到用户缓冲区 copy
  • 接下来以读数据为例,讲述五种 IO 之间的区别。

    • 这五种 IO 的不同之处就在于对 “影响 IO 效率的两个主要因素” 的处理方式不同。

3.2.2 阻塞IO

  • 顾名思义,阻塞 IO 就是用户进程在两个阶段都必须阻塞等待

3.2.3 非阻塞IO

  • 顾名思义,非阻塞 IO 的 recvfrom 操作会立即返回结果而不是阻塞用户进程

    • 一阶段时是非阻塞,内核会直接返回错误信息。

      二阶段时是阻塞,等待数据从内核缓冲区拷贝到用户缓冲区。

  • 其实没啥用,因为用户进程一直反复调用 recvfrom,什么都没干,相当于阻塞。而且忙等机制会导致 CPU 空转,CPU 使用率暴增。

3.2.4 ★★★IO多路复用

  • 无论是阻塞 IO 还是非阻塞 IO,用户应用在一阶段都需要调用 recvfrom 来获取数据,差别在于无数据时的处理方案:

    • 如果调用 recvfrom 时,恰好没有数据,阻塞 IO 会使进程阻塞,非阻塞 IO 会使 CPU 空转,都不能充分发挥CPU的作用。
    • 如果调用 recvfrom 时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据。

    考虑这样一种情况:服务端处理客户端 socket 请求时,在单线程情况下,只能依次处理每一个 socket,如果正在处理的 socket 恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有其它客户端 socket 都必须等待,性能自然会很差。

    • 为了解决这个问题,可以不让客户端的 socket 请求排队等待,而是看哪个 socket 请求的数据在内核态中已经准备就绪了,如果就绪了,用户应用就去内核缓冲区读取相应的 socket 请求的数据。
      • 问题在于:用户进程如何知道内核中数据是否就绪呢?
        • 通过 FD 知道。
  • 文件描述符(File Descriptor):简称 FD,是一个从 0 开始递增的无符号整数,用来关联 Linux 中的一个文件。在 Linux
    中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。

    • IO 多路复用就是利用单个线程来同时监听多个 FD,并在某个 FD 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源。

      • 不过监听 FD 的方式,通知的方式又有多种实现,常见的有:

        • select
        • poll
        • epoll

        selectpoll 只会通知用户进程有 FD 就绪,但不确定具体是哪个 FD,需要用户进程逐个遍历 FD 来确认。

        epoll 则会在通知用户进程 FD 就绪的同时,把已就绪的 FD 写入用户空间

3.2.4.1 select
  • select 是 Linux 中最早的 l/O 多路复用实现方案:

    • select 模式存在的问题:
      • 需要将整个 fd_set 从用户空间拷贝到内核空间,select 结束还要再次拷贝回用户空间。
      • select 无法得知具体是哪个 fd 就绪,需要遍历整个 fd_set
      • fd_set 监听的 fd 数量不能超过 1024。
3.2.4.2 poll
  • poll 模式对 select 模式做了简单改进,但性能提升不明显,部分关键代码如下:
  • select 模式存在的问题在 poll 模式中依然存在,只解决了 fd 数量限制的问题,而且还可能带来性能的下降。
3.2.4.3 ★epoll
  • epoll 模式是对 selectpoll 的改进,它提供了三个函数:

    1. epoll_create:创建 eventpolleventpoll 包含有一颗红黑树和一个链表。
    2. epoll_ctl:将一个 FD 添加到 epoll 的红黑树中,并设置 ep_poll_callbackcallback 触发时,就把对应的 FD 加入到 rdlist 这个就绪列表中
    3. epoll_wait:检查 rdlist 列表是否为空,不为空则返回就绪的 FD 的数量,同时,rdlist 中就绪的 FD 拷贝到 events 空数组中,这样用户空间就明确知道有哪些 FD 的数据就绪了
    • 相比于 selectpollepoll 解决了前两种模式存在的问题:
      1. 基于 epoll 实例中的红黑树保存要监听的 FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的 FD 数量增多而下降。
      2. 每个 FD 只需要执行一次 epoll_ctl 添加到红黑树,以后每次 epoll_wait 无需传递任何参数,无需重复拷贝 FD 到内核空间。
      3. 内核会将就绪的 FD 直接拷贝到用户空间的指定位置,用户进程无需遍历所有 FD 就能知道就绪的 FD 是谁
事件通知机制
  • FD 有数据可读时,我们调用 epoll_wait 就可以得到通知。事件通知的模式一般有两种:

    • LevelTriggered:简称 LT。当 FD 有数据可读时,会重复通知多次,直至数据处理完成。是 epoll 的默认模式。
    • EdgeTriggered:简称 ET。当 FD 有数据可读时,只会被通知一次,不管数据是否处理完成。
      • ET 模式避免了 LT 模式可能出现的惊群现象。
      • ET 模式最好结合非阻塞 IO 读取 FD 数据,相比 LT 会复杂一些。

    例子见视频 P167

web服务流程
  • 基于 epoll 模式的 web 服务的基本流程如图:

3.2.5 信号驱动IO

  • 信号驱动 IO 是与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 就绪时,会发出 SIGIO 信号通知用户,期间用户应用可以执行其他业务,无需阻塞等待(相比于非阻塞 IO,这是真正的 “非阻塞”)。

    • 缺点:
    • 当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致信号队列溢出。而且内核空间与用户空间的频繁信号交互性能也较低。

3.2.6 异步IO

  • 异步 IO 的整个过程都是非阻塞的,用户进程调用完异步 API 后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。

    可以看到,异步 IO 模型中,用户进程在两个阶段都是非阻塞状态。

    • 缺点:

      • 因为进程不阻塞,所以进程会一直接收用户请求,然后进程会一直调用异步 API 请求内核准备数据,而 IO 读写速度一般是比较慢的,所以就会导致内核积累的任务量越来越多。

        • 所以如果采用异步 IO,要做好并发限流机制,那实现起来的复杂度就会提高。

          IO 多路复用既保证了一定的并发性,也不会对内核造成太大压力,实现复杂度也相对低,所以 IO 多路复用用的多,而异步 IO 用的少。

同步和异步
  • IO 操作分为同步和异步,而 IO 操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的 IO 操作),也就是二阶段是同步还是异步

3.2.7 ★★★Redis网络模型

  • Redis 到底是单线程还是多线程?

    • 如果仅仅聊 Redis 的核心业务部分(命令处理),答案是单线程。
    • 如果是聊整个 Redis,那么答案就是多线程。

    在 Redis 版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:

    • Redis v4.0:引入多线程异步处理一些耗时较长的任务,例如异步删除命令 unlink
    • Redis v6.0:在核心网络模型中引入多线程,进一步提高对于多核 CPU 的利用率
  • 为什么 Redis 要选择单线程

    • 抛开持久化不谈,Redis 是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
    • 多线程会导致过多的上下文切换,带来不必要的开销。
    • 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣。

    总结就是:收益不高,成本不低。

  • Redis 通过 IO 多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库 API 库 AE。

Redis单线程网络模型流程
  • 结合视频 P171 看。

  • 简单来说,Redis 单线程网络模型的核心就是 IO 多路复用 + 事件派发

    • 通过 IO 多路复用来监听已经就绪的 FD,对就绪的 FD 进行处理,提高了网络性能。
    • 通过事件派发来将不同的事件派发给不同的处理器:
      • client socket 的注册派发给 server socket 的读处理器 acceptTcpHandler
      • 用户发送的请求派发给 client socket 的读处理器 readQueryFromClient
      • 请求处理的结果派发给 client socket 的写处理器 sendReplyToClient
  • Redis 6.0 版本中引入了多线程,目的是为了提高 IO 读写效率。因此在解析客户端命令、写响应结果时采用了多线程核心的命令执行、IO 多路复用模块依然是由主线程执行

3.3 通信协议

3.3.1 RESP协议

  • Redis 是一个 C/S 架构的软件,通信一般分两步:

    1. 客户端(client)向服务端(server)发送一条命令。(客户端 –请求–> 服务端)
    2. 服务端解析并执行命令,返回响应结果给客户端。(服务端 –响应–> 客户端)

    因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。

    • 而在 Redis 中采用的是 RESP(Redis Serialization Protocol) 协议:

      • Redis 1.2 版本引入了 RESP 协议。
      • Redis 2.0 版本中成为与 Redis 服务端通信的标准,称为 RESP2。
      • Redis 6.0 版本中,从 RESP2 升级到了 RESP3 协议,增加了更多数据类型并且支持 6.0 的新特性 – 客户端缓存。

      由于 RESP3 的兼容性问题,目前,默认使用的依然是 RESP2 协议,也是我们要学习的协议版本(以下简称 RESP )。

数据类型
  • 在 RESP 中,通过首字节的字符来区分不同数据类型,常用的数据类型包括 5 种:

    • 单行字符串:首字节是 +

    • 错误 Errors:首字节是 -

    • 数值:首字节是 :

    • 多行字符串:首字节是 $

    • 数组:首字节是 *

    • 一般来说,客户端给服务端发送的请求,一般都是数组类型

      服务端给客户端发送的响应,就可能为以上五种数据类型

3.4 ★★★内存策略

  • Redis 之所以性能强,最主要的原因就是基于内存存储。然而单节点的 Redis 其内存大小不宜过大,会影响持久化或主从同步性能。

    • 我们可以通过修改配置文件来设置 Redis 的最大内存:

      1
      2
      3
      4
      #格式:
      maxmemory <bytes>
      #例如:
      maxmemory 1gb

      当内存使用达到上限时,就无法存储更多数据了。如果这样的话,当内存达到使用上限后,我们再向其中存数据,不是存不进去了?

      • 所以 Redis 有自己的内存策略来释放内存。

3.4.1 过期key处理(内存过期策略)

  • 在学习 Redis 缓存的时候我们说过,可以通过 expire 命令给 Redis 的 key 设置 TTL(存活时间),当 key 的 TTL 到期以后,再次访问 name 返回的是 nil,说明这个 key 已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。
    • 这里有两个问题需要我们思考:
      • Redis 是如何知道一个 key 是否过期呢?
        • 利用两个 Dict 分别记录 key-value 以及 key-TTL
      • 是不是 TTL 到期就立即删除了呢?
        • 惰性删除
        • 周期删除
Redis的DB结构
  • Redis 本身是一个典型的 key-value 内存存储数据库,因此所有的 key、value 都保存在之前学习过的 Dict 结构中(一共有 16 个这样的 DB:SELECT 0 就是选择第 1 个 DB)。不过在其 database 结构体中,有两个主要的 Dict:一个用来记录 key-value,另一个用来记录 key-TTL
惰性删除
  • 顾名思义,并不是在 TTL 到期后就立刻删除,而是在访问一个 key 的时候,检查该 key 的存活时间,如果已经过期才执行删除

    • 惰性删除有一个问题:如果 key 已经过期了,但是后面一直没有被访问,那这个过期的 key 就会一直存在,不会被删除,而在极端情况下,如果有大量这样的过期 key 未被删除,就会占用 Redis 大量的内存空间。
周期删除
  • 顾名思义,通过一个定时任务,周期性的抽样部分过期的 key,然后执行删除。执行周期有两种:

    • Redis 会设置一个定时任务 serverCron() ,按照 server.hz 的频率来执行过期 key 清理,**模式为 SLOW**。

      1. 执行频率受 server.hz 影响,默认为 10,即每秒执行 10 次,每个执行周期 100ms。
      2. 执行清理耗时不超过一次执行周期的 25%。
      3. 逐个遍历 db,逐个遍历 db 中的 bucket,抽取 20 个 key 判断是否过期。
      4. 如果没达到时间上限(25ms)并且过期 key 比例大于 10%,再进行一次抽样,否则结束。

      低频高时长

    • Redis 的每个事件循环前会调用 beforeSleep() 函数,执行过期 key 清理,**模式为 FAST**。

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

      高频低时长

3.4.2 内存淘汰策略

  • 内存淘汰:就是当 Redis 内存使用达到设置的阈值时,Redis 主动挑选部分 key 删除以释放更多内存的流程。

    • 这里同样有两个问题需要我们思考:

      • Redis 什么时候知道内存使用达到了设置的阈值呢?

        • Redis 会在每次处理客户端命令的方法 porocessCommand() 时尝试做内存淘汰,在这里判断内存的使用是否达到了设置的阈值。
      • 该淘汰哪些 key 呢?

        • Redis 支持 8 种不同策略来选择要删除的 key。

          LRULFU 的值在哪里知道呢?

          • 还记得 RedisObject 结构体对象吗,就在这里面记录。

            如果采用 LRU 策略,就以秒为单位记录最近一次访问时间,长度 24 bit。

            如果采用 LFU 策略,就高 16 位以分钟为单位记录最近一次访问时间,低 8 位记录逻辑访问次数

---------------The End---------------