前言作为服务器,内存并不是无限的,所以总会有内存耗尽的情况,那么当Redis服务器的内存耗尽时,如果继续执行请求命令,Redis会如何处理呢?内存回收在使用Redis服务时,很多情况下有些键值对只在特定的时间段内有效。为了不让这类数据一直占用内存,我们可以给键值对设置一个有效期。在Redis中,可以通过4个独立的命令来设置键的过期时间:expirekeyttl:设置键值的过期时间为ttl秒。pexpirekeyttl:设置key值的过期时间为ttl毫秒。expireatkeytimestamp:设置key值的过期时间为指定的timestamp秒数。pexpireatkeytimestamp:设置key值的过期时间为指定的timestamp毫秒。PS:无论使用哪个命令,Redis的底层最终都是使用pexpireat命令来实现的。另外,set等命令还可以设置key,同时添加过期时间,可以保证设置value和设置过期时间的原子性。设置有效期后,可以使用ttl和pttl命令查询剩余的过期时间(如果没有设置过期时间,下面两条命令返回-1,如果设置了非法过期时间,都返回-2):ttlkey返回密钥剩余的过期秒数。pttlkey返回密钥过期剩余的毫秒数。过期策略如果一个过期的key被删除,我们一般有三种策略:定时删除:为每个key设置一个定时器,一旦过期时间到了,key就会被删除。这种策略对内存友好,但对CPU不友好,因为每个定时器都会占用一定的CPU资源。懒删除:无论key是否过期,都不主动删除。等到每次获取到key后才判断是否过期。如果过期,删除key,否则返回key对应的值。这种策略对内存不友好,可能会浪费大量内存。定期扫描:系统定期扫描,并删除任何过期的密钥。该策略是上述两种策略的相对折衷。需要注意的是,定时频率要根据实际情况来控制。使用此解决方案的缺点是过期的密钥也可能会被返回。在Redis中选择了策略2和策略3的综合使用。但是Redis的常规扫描只会扫描设置了过期时间的key,因为Redis会把设置了过期时间的key分开存储,所以不会有扫描所有键:typedefstructredisDb{dict*dict;//所有键值对dict*expires;//设置过期时间的键值对dict*blocking_keys;//被阻塞的键,比如客户端执行阻塞指令时这样asBLPOPdict*watched_keys;//WATCHEDkeysintid;//DatabaseID//...省略其他属性}redisDb;8淘汰策略如果Redis中所有的key都没有过期,此时内存已经满了,当client继续执行set等命令时,Redis会怎么做?Redis提供了不同的淘汰策略来应对这种场景。首先,Redis提供了一个参数maxmemory来配置Redis使用的最大内存:maxmemory也可以通过命令configsetmaxmemory1GB动态修改。如果不设置该参数,Redis在32位操作系统上最多可以使用3GB内存,在64位操作系统上没有限制。Redis提供了8种淘汰策略,可以通过参数maxmemory-policy进行配置:淘汰策略说明volatile-lru删除根据LRU算法设置过期时间的键,直到有空闲空间可用。如果没有可删除的key对象,内存还是不够,就会报错。allkeys-lru根据LRU算法删除所有键,直到有可用空间。如果没有可删除的key对象,内存还是不够,就会报错。volatile-lfu会删除根据LFU算法设置过期时间的密钥,直到有空闲空间可用。如果没有可删除的key对象,内存还是不够,就会报错。allkeys-lfu根据LFU算法删除所有键,直到有可用空间为止。如果没有可以删除的key对象,内存还是不够,就会报错。volatile-random随机删除设置了过期时间的密钥,直到可用空间可用。如果没有可以删除的key对象,内存还是不够用,就会报错allkeys-random随机删除所有key,直到有可用空间。如果没有可以删除的key对象,内存还是不够,就会报错。volatile-ttl根据key-value对象的ttl属性,删除最近即将过期的数据。如果没有,就直接报noevictiondefaultpolicy,什么都不做,直接报错PS:淘汰策略也可以直接使用命令configsetmaxmemory-policy动态配置。LRU算法LRU的全称是:LeastRecentlyUsed。即:最近最长时间没有使用。这主要是针对时间的使用。Redis改进的LRU算法并没有使用Redis中传统的LRU算法,因为传统的LRU算法有两个问题:需要额外的空间来存储。可能有一些键值经常被使用,但是最近没有被使用,所以被LRU算法删除。为了避免以上两个问题,Redis对传统的LRU算法进行了修改,采用采样的方式进行删除。配置文件提供了一个属性maxmemory_samples5,默认值为5,意思是随机选择5个键值,然后根据LRU算法删除这5个键值,显然越大键值越大,删除越准确。对于采样LRU算法和传统LRU算法,Redis官网上有一张对比图:浅灰色带是要删除的对象。灰色带是未删除的对象。绿色是添加的对象。左上角第一张图代表传统的LRU算法。可以看到,当样本数达到10时(右上角),已经非常接近传统的LRU算法了。Redis是如何管理热数据的在我们讲到字符串对象的时候,提到了redisObject对象中有一个lru属性:typedefstructredisObject{unsignedtype:4;//对象类型(4位=0.5字节)unsignedencoding:4;//encoding(4位=0.5字节)unsignedlru:LRU_BITS;//记录应用程序最后一次访问对象的时间(24位=3字节)intrefcount;//引用计数。等于0时表示可以进行垃圾回收(32位=4字节)void*ptr;//指向底层实际数据存储结构,如:SDS等(8字节)}robj;lru属性是在创建对象的时候写入的,对象在被访问的时候也会更新。正常的思路是,最终决定是否删除某个key肯定是将当前时间戳减去lru,差值最大的先删除。但是在Redis中却不是这样。Redis维护了一个全局属性lru_clock。该属性通过每100毫秒执行一次全局函数serverCron来更新,并记录当前的Unix时间戳。删除数据的最终决定是通过从lru_clock中减去对象的lru属性获得的。那么Redis为什么要这样做呢?直接取全球时间不是更准确吗?这是因为这样做可以避免每次更新对象的lru属性时直接取全局属性,而不用调用系统函数获取系统时间,从而提高效率(Redis中有很多这样的提高性能的细节,它可以说是把性能优化到了极致)。但是,这里仍然存在问题。我们可以看到redisObject对象中的lru属性只有24位,24位只能存储194天的时间戳大小。一旦超过194天,就会从0重新开始,所以这时候可能会出现redisObject对象中的lru属性大于全局的lru_clock属性。正因如此,计算需要分两种情况:当全局lruclock>lru时,使用lruclock-lru获取空闲时间。当globallruclocknow时,默认为一个周期(16位,最大65535),则取差65535-ldt+now;当lru<=now时,取now-ldt之差(为了后续计算方便,将此差记为idle_time)。4、取出配置文件中的lfu_decay_time值,然后计算:idle_time/lfu_decay_time(为了后续计算方便,将该值记为num_periods)。5.最后,减少计数器:counter-num_periods。看起来好复杂,其实计算公式就是一句话:取出当前时间戳与对象中的lru属性进行比较,计算多久没有被访问过。比如计算出的结果是100分钟没有访问,然后去掉配置参数lfu_decay_time,如果这个配置默认为1,即100/1=100,表示100分钟没有访问分钟,所以计数器会减100。总结本文主要介绍了Redis过期键的处理策略,以及服务器内存不够时的8种Redis淘汰策略,最后介绍了Redis中的两种主要淘汰算法,LRU和LFU。