前言
分析:你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?
先来回答第一个问题,怎么删数据?
内存满了,可以调大一点呀!!!
Redis
中有配置参数maxmemory
可以设置Redis内存的大小。redis.conf
配置maxmemory
的大小参数如下所示:
maxmemory 100G
- 通过命令修改,
Redis
支持运行时通过命令动态修改内存大小。
//设置Redis最大占用内存大小为100M
127.0.0.1:6379> config set maxmemory 100mb
//获取设置的Redis能使用的最大内存大小
127.0.0.1:6379> config get maxmemory
但是实际的存储中超出了Redis
的配置参数的大小时,Redis中有淘汰策略
,把需要淘汰的key给淘汰掉
,整理出干净的一块内存给新的key值使用。
8 种淘汰策略
Redis 中提供了 8 种淘汰策略,可以通过参数 maxmemory-policy
进行配置:
- volatile-lru(最久未使用的): 根据
LRU
算法删除设置了过期时间
的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错; - volatile-lfu(最少使用的键值) 根据
LFU
算法删除设置了过期时间
的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错; - volatile-random 随机删除设置了
过期时间
的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错; - allkeys-lru : 根据
LRU
算法删除所有
的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错; - allkeys-lfu 根据
LFU
算法删除所有
的键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错; - allkeys-random 随机删除所有键,直到腾出可用空间。如果没有可删除的键对象,且内存还是不够用时,则报错;
- volatile-ttl 根据键值对象的 ttl 属性, 删除最近将要过期数据。如果没有,则直接报错;
- noeviction 默认策略,不作任何处理,直接报错。
假如在Redis中的数据有一部分是热点数据,而剩下的数据是冷门数据,或者我们不太清楚我们应用的缓存访问分布状况,这时可以使用allkeys-lru
。
如何修改 Redis 内存淘汰策略?
- 淘汰策略也可以直接使用命令 config set maxmemory-policy <策略> 来进行动态配置。
- 通过修改 Redis 配置文件修改,设置
maxmemory-policy <策略>
,它的优点是重启 Redis 服务后配置不会丢失,缺点是必须重启 Redis 服务,设置才能生效。
LRU算法
LRU(Least Recently Used)即表示最近最少使用,算法根据数据的历史访问记录来进行淘汰数据。它的核心的思想就是:假如一个key值在最近很少被使用到,那么在将来也很少会被访问。
Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
- 需要用链表管理所有的缓存数据,这会带来额外的空间开销;
- 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
Redis使用的是近似的LRU算法,通过随机采集法淘汰key,每次都会随机选出5个key,然后淘汰里面最近最少使用的key。
maxmemory-samples 5
那么为了实现根据时间实现LRU算法,Redis必须为每个key中额外的增加一个内存空间用于存储每个key的时间,3字节。
typedef struct redisObject {
unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
.....
} robj;
Redis 实现的 LRU 算法的优点:
- 不用为所有的数据维护一个大链表,节省了空间占用;
- 不用在每次数据访问时都移动链表项,提升了缓存的性能;
lru 属性是创建对象的时候写入,对象被访问到时也会进行更新
。正常人的思路就是最后决定要不要删除某一个键肯定是用当前时间戳减去 lru
,差值最大的就优先被删除。但是 Redis
里面并不是这么做的,Redis 中维护了一个全局属性 lru_clock
,这个属性是通过一个全局函数 serverCron
每隔 100
毫秒执行一次来更新的,记录的是当前 unix
时间戳。最后决定删除的数据是通过 lru_clock
减去对象的 lru
属性而得出的。
那么为什么 Redis 要这么做呢?直接取全局时间不是更准确吗?
这是因为这么做可以避免每次更新对象的 lru 属性的时候可以直接取全局属性,而不需要去调用系统函数来获取系统时间,从而提升效率。
不过这里还有一个问题,我们看到,redisObject 对象中的 lru 属性只有 24 位,24 位只能存储 194 天的时间戳大小,一旦超过 194 天之后就会重新从 0 开始计算,所以这时候就可能会出现 redisObject 对象中的 lru 属性大于全局的 lru_clock 属性的情况。
正因为如此,所以计算的时候也需要分为 2 种情况:
当全局 lruclock > lru,则使用 lruclock - lru 得到空闲时间。
当全局 lruclock < lru,则使用 lruclock_max(即 194 天) - lru + lruclock 得到空闲时间。
Redis3.0对近似LRU的优化
新算法会维护一个候选池(16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。当放满后,如果有新的key需要放入,则将池中最近被访问的移除。当需要淘汰的时候,则直接从池中选取最近最久没被访问的key淘汰掉就行。
你可以看到图中有三种不同颜色的点:
- 浅灰色是被淘汰的数据。
- 灰色是没有被淘汰掉的老数据。
- 绿色是新加入的数据。
LRU算法有一个弊端:
- 就是假如一个key值在以前都没有被访问到,然而最近一次被访问到了,那么就会认为它是热点数据,不会被淘汰。
- 然而有些数据以前经常被访问到,只是最近的时间内没有被访问到,这样就导致这些数据很可能被淘汰掉,这样一来就会出现误判而淘汰热点数据。
LFU 算法
LFU即表示最近最不常用的,以最近的时间段的被访问次数的频率作为一种判断标准。它的核心思想就是:根据key最近被访问的频率进行淘汰,比较少被访问的key优先淘汰。
当我们采用LFU
回收策略时,lru
属性的高16
位用来记录访问时间(ldt
),低 8
位用来记录访问频率 (logic
)。
ldt
是用来记录 key 的访问时间戳;logc
是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的logc 初始值为 5。logc
是访问频次,因为logc
会随时间推移而衰减的。
lfu算法 key 频率变化的流程
- 在每次
key
被访问时,会先对logc
做一个衰减操作,衰减的值跟前后访问时间的差距有关系,如果上一次访问的时间与这一次访问的时间差距很大,那么衰减的值就越大。 - 对
logc
做完衰减操作后,就开始对logc
进行增加操作,增加操作并不是单纯的+ 1
,而是根据概率增加,如果logc
越大的key
,它的 logc 就越难再增加。
redis.conf
提供了两个配置项,用于调整LFU
算法从而控制 logc
的增长和衰减:
lfu-decay-time
用于调整 logc 的衰减速度,它是一个以分钟为单位的数值,默认值为1,lfu-decay-time
值越大,衰减越慢;
lfu-log-factor
用于调整 logc 的增长速度,lfu-log-factor
值越大,logc
增长越慢。
时间到了,内存占用率还是比较高?
过期时间
使用Redis 服务时,为了防止数据一直占有内存,我们可以给键值对设置有效期。Redis 中可以通过 4 个独立的命令来给一个键设置过期时间:
expire key ttl:将 key 值的过期时间设置为 ttl 秒。
pexpire key ttl:将 key 值的过期时间设置为 ttl 毫秒。
expireat key timestamp:将 key 值的过期时间设置为指定的 timestamp 秒数。
pexpireat key timestamp:将 key 值的过期时间设置为指定的 timestamp 毫秒数。
设置了有效期后,可以通过 ttl 和 pttl 两个命令来查询剩余过期时间(如果未设置过期时间则下面两个命令返回 -1,如果设置了一个非法的过期时间,则都返回 -2):
ttl key 返回 key 剩余过期秒数。
pttl key 返回 key 剩余过期的毫秒数。
过期删除策略
- 定时删除:为每个键设置一个定时器,一旦过期时间到了,则将键删除。这种策略对内存很友好,但是对 CPU 不友好,因为每个定时器都会占用一定的 CPU 资源。
- 惰性删除:不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。
- 定期扫描:系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。
在 Redis 当中,其选择的是惰性删除和定期扫描的综合使用。不过 Redis 的定期扫描只会扫描设置了过期时间的键,因为设置了过期时间的键 Redis 会单独存储。
如何判定 key 已过期了?
每当我们对一个 key
设置了过期时间时,Redis 会把该key
带上过期时间存储到一个过期字典
中,也就是说过期字典
保存了数据库中所有 key 的过期时间。
过期删除策略和淘汰策略的区别?
Redis 使用的过期删除策略是惰性删除+定期删除,删除的对象是已过期的 key;
内存淘汰策略是解决内存过大的问题,当 Redis 的运行内存超过最大运行内存时,就会触发内存淘汰策略。