如果有一天当你的 Redis 内存满了,该怎么办?
简介
我们知道 redis 是一个非常常用的内存型数据库,数据从内存中读取是它非常高效的原因之一,那么但是如果有一天,「redis 分配的内存满了怎么办」?遇到这个面试题不要慌,这种问题我们分为两角度回答就可以:
「redis 会怎么做」?
「我们可以怎么做」?
增加 redis 可用内存
这种方法很暴力,也很好用,我们直接通过增加 redis 的可用内存就可以了, 有两种方式
「通过配置文件配置」//设置 redis 最大占用内存大小为 1000M
maxmemory 1000mb 通过在 redis 安装目录下面的 redis.conf 配置文件中添加以下配置设置内存大小
「通过命令修改」//设置 redis 最大占用内存大小为 1000M
127.0.0.1:6379> config set maxmemory 1000mbredis 支持运行时通过命令动态修改内存大小
这种方法是立竿见影的,reids 内存总归受限于机器的内存,也不能无限制的增长,那么如果没有办法再增加 redis 的可用内存怎么办呢?
内存淘汰策略
实际上 Redis 定义了「8 种内存淘汰策略」用来处理 redis 内存满的情况:
1.noeviction:直接返回错误,不淘汰任何已经存在的 redis 键 2.allkeys-lru:所有的键使用 lru 算法进行淘汰 3.volatile-lru:有过期时间的使用 lru 算法进行淘汰 4.allkeys-random:随机删除 redis 键 5.volatile-random:随机删除有过期时间的 redis 键 6.volatile-ttl:删除快过期的 redis 键 7.volatile-lfu:根据 lfu 算法从有过期时间的键删除 8.allkeys-lfu:根据 lfu 算法从所有键删除
这些内存淘汰策略都很好理解,我们着重讲解一下 lru,lfu,ttl 是怎么去实现的
lru 的最佳实践?
lru 是 Least Recently Used 的缩写,也就是「最近很少使用」,也可以理解成最久没有使用。最近刚刚使用过的,后面接着会用到的概率也就越大。由于内存是非常金贵的,导致我们可以存储在缓存当中的数据是有限的。比如说我们固定只能存储 1w 条,当内存满了之后,缓存每插入一条新数据,都要抛弃一条最长没有使用的旧数据。我们把上面的内容整理一下,可以得到几点要求:
「1.保证其的读写效率,比如读写的复杂度都是 O(1)」
「2.当一条数据被读取,将它最近使用的时间更新」
「3.当插入一条新数据的时候,删除最久没有使用过的数据」
所以我们要尽可能的保证查询效率很高,插入效率很高,我们知道如果只考虑查询效率,那么 hash 表可能就是最优的选择,如果只考虑插入效率,那么链表必定有它的一席之地。
但是这两种数据结构单独使用,都有它的弊端,那么说,有没有一种数据结构,既能够保证查询效率,又能够保证插入效率呢?于是 hash+链表这种结构出现了
hash 表用来查询在链表中的数据位置,链表负责数据的插入 当新数据插入到链表头部时有两种情况;
1.当链表满的时候,将链表尾部的数据丢弃。这个比较简单,直接将链表尾部指针抹去,并且清除对应 hash 中的信息就好了
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;这种情况我们发现,如果命中到链表中间节点,我们需要做的是 1).将该节点移到头节点 2).「将该节点的上一个节点的下一个节点,设置为该节点的下一个节点」,这里就会有一个问题,我们无法找到该节点的上一个节点,因为是单向链表,所以,新的模型产生了。
这时双向链表的作用也提现出来了。能直接定位到父节点。这效率就很高了。而且由于双向链表有尾指针,所以剔除最后的尾节点也十分方便,快捷
所以最终的解决方案就是采用「哈希表+双向链表」的结构
lfu 的最佳实践?
LFU:Least Frequently Used,最不经常使用策略,在一段时间内,数据被「使用频次最少」的,优先被淘汰。最少使用(LFU)是一种用于管理计算机内存的缓存算法。主要是记录和追踪内存块的使用次数,当缓存已满并且需要更多空间时,系统将以最低内存块使用频率清除内存.采用 LFU 算法的最简单方法是为每个加载到缓存的块分配一个计数器。每次引用该块时,计数器将增加一。当缓存达到容量并有一个新的内存块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除。
这里我们提出一种达到 O(1) 时间复杂度的 LFU 实现方案,它支持的操作包括插入、访问以及删除
如图:
由两个双向链表+哈希表组成,上方的双向链表用来计数,下方的双向链表用来记录存储的数据,该链表的头节点存储了数字,哈希表的 value 对象记录下方双向链表的数据 我们这里按照插入的流程走一遍:
将需要存储的数据插入
在 hash 表中「存在」,找到对应的下方双向链表,将该节点的上一个节点和该节点的下一个节点相连(这里可能只有自己,直接移除就好),然后判断自己所在上方双向链表的计数是否比当前计数大 1「如果是」,则将自己移到该上方双向链表,并且「判断该双向链表下是否还有元素」,如果没有,则要删除该节点「如果不是或者该上方双向列表无下个节点」则新加节点,将计数设为当前计数+1
在 hash 表「不存在」,将数据存入 hash 表,将数据与双向链表的头节点相连(这里有可能链表未初始化)
这样当查找,插入时效率都为 O(1)
redis TTL 是怎么实现的?
TTL 存储的数据结构
redis 针对 TTL 时间有专门的 dict 进行存储,就是 redisDb 当中的 dict *expires 字段,dict 顾名思义就是一个 hashtable,key 为对应的 rediskey,value 为对应的 TTL 时间。 dict 的数据结构中含有 2 个 dictht 对象,主要是为了解决 hash 冲突过程中重新 hash 数据使用。
TTL 设置过期时间
TTL 设置 key 过期时间的方法主要是下面 4 个:
expire 按照相对时间且以秒为单位的过期策略
expireat 按照绝对时间且以秒为单位的过期策略
pexpire 按照相对时间且以毫秒为单位的过期策略
pexpireat 按照绝对时间且以毫秒为单位的过期策略
expire expireat pexpire pexpireat
从实际设置过期时间的实现函数来看,相对时间的策略会有一个当前时间作为基准时间,绝对时间的策略会「以 0 作为一个基准时间」。
整个过期时间最后都会换算到绝对时间进行存储,通过公式基准时间+过期时间来进行计算。 对于相对时间而言基准时间就是当前时间,对于绝对时间而言相对时间就是 0。 中途考虑设置的过期时间是否已经过期,如果已经过期那么在 master 就会删除该数据并同步删除动作到 slave。 正常的设置过期时间是通过 setExpire 方法保存到 dict *expires 对象当中。
redis 什么时候执行淘汰策略?
在 redis 种有三种删除的操作此策略
定时删除:对于设有过期时间的 key,时间到了,定时器任务立即执行删除因为要维护一个定时器,所以就会占用 cpu 资源,尤其是有过期时间的 redis 键越来越多损耗的性能就会线性上升
惰性删除:每次只有再访问 key 的时候,才会检查 key 的过期时间,若是已经过期了就执行删除。这种情况只有在访问的时候才会删除,所以有可能有些过期的 redis 键一直不会被访问,就会一直占用 redis 内存
定期删除:每隔一段时间,就会检查删除掉过期的 key。这种方案相当于上述两种方案的折中,通过最合理控制删除的时间间隔来删除 key,减少对 cpu 的资源的占用消耗,使删除操作合理化。
评论