做为一台服务器来讲,内存并非无限的,因此总会存在内存耗尽的状况,那么当 Redis
服务器的内存耗尽后,若是继续执行请求命令,Redis
会如何处理呢?java
使用Redis
服务时,不少状况下某些键值对只会在特定的时间内有效,为了防止这种类型的数据一直占有内存,咱们能够给键值对设置有效期。Redis
中能够经过 4
个独立的命令来给一个键设置过时时间:redis
expire key ttl
:将 key
值的过时时间设置为 ttl
秒。pexpire key ttl
:将 key
值的过时时间设置为 ttl
毫秒。expireat key timestamp
:将 key
值的过时时间设置为指定的 timestamp
秒数。pexpireat key timestamp
:将 key
值的过时时间设置为指定的 timestamp
毫秒数。PS:无论使用哪个命令,最终 Redis
底层都是使用 pexpireat
命令来实现的。另外,set
等命令也能够设置 key
的同时加上过时时间,这样能够保证设值和设过时时间的原子性。算法
设置了有效期后,能够经过 ttl
和 pttl
两个命令来查询剩余过时时间(若是未设置过时时间则下面两个命令返回 -1
,若是设置了一个非法的过时时间,则都返回 -2
):服务器
ttl key
返回 key
剩余过时秒数。pttl key
返回 key
剩余过时的毫秒数。若是将一个过时的键删除,咱们通常都会有三种策略:dom
CPU
不友好,由于每一个定时器都会占用必定的 CPU
资源。在 Redis
当中,其选择的是策略 2
和策略 3
的综合使用。不过 Redis
的按期扫描只会扫描设置了过时时间的键,由于设置了过时时间的键 Redis
会单独存储,因此不会出现扫描全部键的状况:函数
typedef struct redisDb { dict *dict; //全部的键值对 dict *expires; //设置了过时时间的键值对 dict *blocking_keys; //被阻塞的key,如客户端执行BLPOP等阻塞指令时 dict *watched_keys; //WATCHED keys int id; //Database ID //... 省略了其余属性 } redisDb;
假如 Redis
当中全部的键都没有过时,并且此时内存满了,那么客户端继续执行 set
等命令时 Redis
会怎么处理呢?Redis
当中提供了不一样的淘汰策略来处理这种场景。性能
首先 Redis
提供了一个参数 maxmemory
来配置 Redis
最大使用内存:优化
maxmemory <bytes>
或者也能够经过命令 config set maxmemory 1GB
来动态修改。编码
若是没有设置该参数,那么在 32
位的操做系统中 Redis
最多使用 3GB
内存,而在 64
位的操做系统中则不做限制。操作系统
Redis
中提供了 8
种淘汰策略,能够经过参数 maxmemory-policy
进行配置:
淘汰策略 | 说明 |
---|---|
volatile-lru | 根据 LRU 算法删除设置了过时时间的键,直到腾出可用空间。若是没有可删除的键对象,且内存仍是不够用时,则报错 |
allkeys-lru | 根据 LRU 算法删除全部的键,直到腾出可用空间。若是没有可删除的键对象,且内存仍是不够用时,则报错 |
volatile-lfu | 根据 LFU 算法删除设置了过时时间的键,直到腾出可用空间。若是没有可删除的键对象,且内存仍是不够用时,则报错 |
allkeys-lfu | 根据 LFU 算法删除全部的键,直到腾出可用空间。若是没有可删除的键对象,且内存仍是不够用时,则报错 |
volatile-random | 随机删除设置了过时时间的键,直到腾出可用空间。若是没有可删除的键对象,且内存仍是不够用时,则报错 |
allkeys-random | 随机删除全部键,直到腾出可用空间。若是没有可删除的键对象,且内存仍是不够用时,则报错 |
volatile-ttl | 根据键值对象的 ttl 属性, 删除最近将要过时数据。 若是没有,则直接报错 |
noeviction | 默认策略,不做任何处理,直接报错 |
PS:淘汰策略也能够直接使用命令 config set maxmemory-policy <策略>
来进行动态配置。
LRU
全称为:Least Recently Used
。即:最近最长时间未被使用。这个主要针对的是使用时间。
在 Redis
当中,并无采用传统的 LRU
算法,由于传统的 LRU
算法存在 2
个问题:
key
值使用很频繁,可是最近没被使用,从而被 LRU
算法删除。为了不以上 2
个问题,Redis
当中对传统的 LRU
算法进行了改造,经过抽样的方式进行删除。
配置文件中提供了一个属性 maxmemory_samples 5
,默认值就是 5
,表示随机抽取 5
个 key
值,而后对这 5
个 key
值按照 LRU
算法进行删除,因此很明显,key
值越大,删除的准确度越高。
对抽样 LRU
算法和传统的 LRU
算法,Redis
官网当中有一个对比图:
浅灰色带是被删除的对象。
灰色带是未被删除的对象。
绿色是添加的对象。
左上角第一幅图表明的是传统 LRU
算法,能够看到,当抽样数达到 10
个(右上角),已经和传统的 LRU
算法很是接近了。
前面咱们讲述字符串对象时,提到了 redisObject
对象中存在一个 lru
属性:
typedef struct redisObject { unsigned type:4;//对象类型(4位=0.5字节) unsigned encoding:4;//编码(4位=0.5字节) unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节) int refcount;//引用计数。等于0时表示能够被垃圾回收(32位=4字节) void *ptr;//指向底层实际的数据存储结构,如:SDS等(8字节) } robj;
lru
属性是建立对象的时候写入,对象被访问到时也会进行更新。正常人的思路就是最后决定要不要删除某一个键确定是用当前时间戳减去 lru
,差值最大的就优先被删除。可是 Redis
里面并非这么作的,Redis
中维护了一个全局属性 lru_clock
,这个属性是经过一个全局函数 serverCron
每隔 100
毫秒执行一次来更新的,记录的是当前 unix
时间戳。
最后决定删除的数据是经过 lru_clock
减去对象的 lru
属性而得出的。那么为何 Redis
要这么作呢?直接取全局时间不是更准确吗?
这是由于这么作能够避免每次更新对象的 lru
属性的时候能够直接取全局属性,而不须要去调用系统函数来获取系统时间,从而提高效率(Redis
当中有不少这种细节考虑来提高性能,能够说是对性能尽量的优化到极致)。
不过这里还有一个问题,咱们看到,redisObject
对象中的 lru
属性只有 24
位,24
位只能存储 194
天的时间戳大小,一旦超过 194
天以后就会从新从 0
开始计算,因此这时候就可能会出现 redisObject
对象中的 lru
属性大于全局的 lru_clock
属性的状况。
正由于如此,因此计算的时候也须要分为 2
种状况:
lruclock
> lru
,则使用 lruclock
- lru
获得空闲时间。lruclock
< lru
,则使用 lruclock_max
(即 194
天) - lru
+ lruclock
获得空闲时间。须要注意的是,这种计算方式并不能保证抽样的数据中必定能删除空闲时间最长的。这是由于首先超过 194
天还不被使用的状况不多,再次只有 lruclock
第 2
轮继续超过 lru
属性时,计算才会出问题。
好比对象 A
记录的 lru
是 1
天,而 lruclock
第二轮都到 10
天了,这时候就会致使计算结果只有 10-1=9
天,实际上应该是 194+10-1=203
天。可是这种状况能够说又是更少发生,因此说这种处理方式是可能存在删除不许确的状况,可是自己这种算法就是一种近似的算法,因此并不会有太大影响。
LFU
全称为:Least Frequently Used
。即:最近最少频率使用,这个主要针对的是使用频率。这个属性也是记录在redisObject
中的 lru
属性内。
当咱们采用 LFU
回收策略时,lru
属性的高 16
位用来记录访问时间(last decrement time:ldt,单位为分钟),低 8
位用来记录访问频率(logistic counter:logc),简称 counter
。
LFU
计数器每一个键只有 8
位,它能表示的最大值是 255
,因此 Redis
使用的是一种基于几率的对数器来实现 counter
的递增。r
给定一个旧的访问频次,当一个键被访问时,counter
按如下方式递增:
0
和 1
之间的随机数 R
。counter
- 初始值(默认为 5
),获得一个基础差值,若是这个差值小于 0
,则直接取 0
,为了方便计算,把这个差值记为 baseval
。P
计算公式为:1/(baseval * lfu_log_factor + 1)
。R < P
时,频次进行递增(counter++
)。公式中的 lfu_log_factor
称之为对数因子,默认是 10
,能够经过参数来进行控制:
lfu_log_factor 10
下图就是对数因子 lfu_log_factor
和频次 counter
增加的关系图:
能够看到,当对数因子 lfu_log_factor
为 100
时,大概是 10M(1000万)
次访问才会将访问 counter
增加到 255
,而默认的 10
也能支持到 1M(100万)
次访问 counter
才能达到 255
上限,这在大部分场景都是足够知足需求的。
若是访问频次 counter
只是一直在递增,那么早晚会所有都到 255
,也就是说 counter
一直递增不能彻底反应一个 key
的热度的,因此当某一个 key
一段时间不被访问以后,counter
也须要对应减小。
counter
的减小速度由参数 lfu-decay-time
进行控制,默认是 1
,单位是分钟。默认值 1
表示:N
分钟内没有访问,counter
就要减 N
。
lfu-decay-time 1
具体算法以下:
16
位(为了方便后续计算,这个值记为 now
)。lru
属性中的高 16
位(为了方便后续计算,这个值记为 ldt
)。lru
> now
时,默认为过了一个周期(16
位,最大 65535
),则取差值 65535-ldt+now
:当 lru
<= now
时,取差值 now-ldt
(为了方便后续计算,这个差值记为 idle_time
)。lfu_decay_time
值,而后计算:idle_time / lfu_decay_time
(为了方便后续计算,这个值记为num_periods
)。counter
减小:counter - num_periods
。看起来这么复杂,其实计算公式就是一句话:取出当前的时间戳和对象中的 lru
属性进行对比,计算出当前多久没有被访问到,好比计算获得的结果是 100
分钟没有被访问,而后再去除配置参数 lfu_decay_time
,若是这个配置默认为 1
也便是 100/1=100
,表明 100
分钟没访问,因此 counter
就减小 100
。
本文主要介绍了 Redis
过时键的处理策略,以及当服务器内存不够时 Redis
的 8
种淘汰策略,最后介绍了 Redis
中的两种主要的淘汰算法 LRU
和 LFU
。