Redis是一个内存键值对数据库,所以对于内存的管理尤为重要。Redis内部对于内存的管理主要包含两个方向,过期删除策略和数据淘汰策略。思考:
- 什么是数据淘汰?
- 数据过期和数据淘汰都是删除数据,两者有什么区别?
- 实际使用场景是多样化的,如何选择合适的淘汰策略?
淘汰策略原理
所谓数据淘汰是指在Redis内存使用达到一定阈值的时候,执行某种策略释放内存空间,以便于接收新的数据。内存可使用空间由配置参数maxmemory决定(单位mb/GB)。故又叫"最大内存删除策略",也叫"缓存删除策略"。
maxmemory配置
注:若`maxmemory=0`则表示不做内存限制,但是对于windows系统来说,32位系统默认可使用空间是3G,因为整个系统内存是4G,需要留1G给系统运行。且淘汰策略会自动设置为noeviction,即不开启淘汰策略,当使用空间达到3G的时候,新的内存请求会报错。
淘汰策略分类
- 淘汰策略配置maxmemory-policy,表示当内存达到maxmemory时,将执行配置的淘汰策略,由redis.c/freeMemoryIfNeeded 函数实现数据淘汰逻辑。maxmemory-policy配置
freeMemoryIfNeeded逻辑处理
8种淘汰策略
- Redis定义的策略常量(version < 4.0)
3.0版本提供6种策略:
4.0以上版本增加两种LFU策略:
volatile-lfu( REDIS_MAXMEMORY_VOLATILE_LFU): Evict using approximated LFU, only keys with an expire set -> 对配置了过期时间的key,淘汰最近使用频率最少的数据。
allkeys-lfu(REDIS_MAXMEMORY_ALLKEYS_LFU): Evict any key using approximated LFU -> 对所有key,淘汰最近使用频率最少的数据。
volatile-lru( REDIS_MAXMEMORY_VOLATILE_LRU): Evict using approximated LRU, only keys with an expire set -> 内存不足时,对所有配置了过期时间的key,淘汰最近最少使用的数据。
allkeys-lru(REDIS_MAXMEMORY_ALLKEYS_LRU): Evict any key using approximated LRU -> 内存不足时,对所有key,淘汰最近最少使用的数据。
volatile-random( REDIS_MAXMEMORY_VOLATILE_RANDOM): Remove a random key having an expire set -> 内存不足时,对所有配置了过期时间的key,淘汰随机数据。
allkeys-random(REDIS_MAXMEMORY_ALLKEYS_RANDOM): Remove a random key, any key -> 内存不足时,对所有key,淘汰随机数据。
volatile-ttl( REDIS_MAXMEMORY_VOLATILE_TTL): Remove the key with the nearest expire time (minor TTL) -> 内存不足时,对所有配置了过期时间的key,淘汰最近将要过期的数据。
noeviction( REDIS_MAXMEMORY_NO_EVICTION): Don't evict anything, just return an error on write operations -> 不开启淘汰策略,在不配置淘汰策略的情况下,maxmemory-policy默认等于该值。内存不足时,会抛出异常,写操作不可用。不同系统存在差异性-具体见⇑
淘汰策略的选择
- 存在冷热数据区别,即意味着访问频率存在较大差异,4.0及以上版本建议选择allkeys-lfu策略,但要设置lfu-decay-time 计数衰减值,一般默认1,这样可避免缓存污染现象;3.0及以下版本建议选择allkeys-lru策略。LFU访问计数衰减配置
- 若整体访问频率较为平衡,则可选择allkeys-random策略随机淘汰数据。
- 存在置顶数据(或者希望一些数据长期被保存) ,4.0及以上版本建议选择volatile-lfu策略,3.0及以下版本建议选择volatile-lru策略。对于需要置顶的数据不设置或者设置较长的过期时间,其他数据都设置小于该值的过期时间,以便淘汰非置顶数据。
- 若希望所有的数据可通过过期时间来判断其顺序,则可选择volatile-ttl策略。
- 由于过期删除策略的存在,对于过期时间的配置,存在额外的expires字典表,是会占用部分Redis内存的。若希望内存可以得到更加高效的利用,可选择allkeys-lru/allkeys-lfu策略。
Redis在实现淘汰策略时为了更合理的利用内存空间以及保证Redis的高性能,只是几近于算法的实现机制,其会从性能和可靠性层面做出一些平衡,故并不是完全可靠的。因此我们在实际使用过程中,建议都配置过期时间,主动删除那些不再使用的数据,以保证内存的高效使用。另外关于LRU和LFU算法,Redis内部在数据结构和实现机制上都做了一定程度的适应性改造
过期策略原理分析
众所周知,在Redis的实际使用过程中,为了让可贵的内存得到更高效的利用,我们提倡给每一个key配置合理的过期时间,以避免因内存不足,或因数据量过大而引发的请求响应延迟甚至是不可用等问题。思考:
- key的删除是实时的吗?
- 是否存在并发和数据一致性问题?
- 内存空间是有限的,除了过期策略,Redis还有什么其他保障?
过期Key删除原理
过期时间底层原理
当key设置了过期时间,Redis内部会将这个key带上过期时间放入过期字典(expires)中,当进行查询时,会先在过期字典中查询是否存在该键,若存在则与当前UNIX时间戳做对比来进行过期时间判定。
过期时间配置命令如下(即EX|PX|EXAT|PXAT):
这四个命令看似有差异,但在RedisDb底层,最终都会转换成pexpireat指令。内部由db.c/expireGenericCommand函数实现,对外由上面四个指令调用
- 过期字典内部存储结构:key表示一个指向具体键的指针,value是long类型的毫秒精度的UNIX时间戳。
- Rediskey过期时间内部流程图:
图片
常见删除方式
- 定时删除:在写入key之后,根据否配置过期时间生成特定的定时器,定时器的执行时间就是具体的过期时间。用CPU性能换去内存存储空间——即用时间获取空间
- 定期删除:提供一个固定频率的定时器,执行时扫描所有的key进行过期检查,满足条件的就进行删除。
- 惰性删除:数据不做及时释放,待下一次接收到读写请求时,先进行过期检查,若已过期则直接删除。用内存存储空间换取CPU性能——即用空间换取时间
删除方式 | 优点 | 缺点 |
定时删除 | 能及时释放内存空间,不会产生滞留数据 | 频繁生成和销毁定时器,非常损耗CPU性能,影响响应时间和指令吞吐量 |
定期删除 | 固定的频率进行过期检查,对CPU交友好 | 1.数据量比较大的情况下,会因为全局扫描而损耗CPU性能,且主线程的阻塞会导致其他请求响应延迟。2.未能及时释放内存空间。3.数据已过期,但定时器未执行时会导致数据不一致。 |
惰性删除 | 节约CPU性能 | 当某些数据长时间无请求访问时,会导致数据滞留,使内存无法释放,占用内存空间,甚至坑导致内存泄漏而引发服务不可用 |
Redis过期删除策略
由上述三种常用的删除方式对比结果可知,单独的使用任何一种方式都不能达到比较理想的结果,因此Redis的作者在设计过期删除策略的时候,结合了定期删除与惰性删除两种方式来完成。
定期删除:内部通过redis.c/activeExpireCycle函数,以一定的频率运行,每次运行从数据库中随机抽取一定数量的key进行过期检查,若检查通过,则对该数据进行删除。在2.6版本中,默认每秒10次,在2.8版本后可通过redis.config配置文件的hz属性对频率进行设置,,官方建议数值不要超过100,否则将对CPU性能有重大影响。
惰性删除:内部通过redis.c/expireIfNeeded函数,在每次执行读写操作指令之前,进行过期检查。若已设置过期时间且已过期,则删除该数据。
删除方式 | 优点 | 缺点 |
Redis定期删除 | 避免了全局扫描,每次随机抽取数据量较少,性能较稳定,执行频率可配置;避免了惰性删除低频数据长时间滞留的问题 | 存在概率上某些数据一直没被抽取的情况,导致数据滞留 |
Redis惰性删除 | 解决了定期删除可能导致的数据滞留现象,性能较高 | 低频数据长时间无法释放 |
总结:由表格可知,这两种方式的结合,能很好的解决过期数据滞留内存的问题,同时也很好的保证了数据的一致性,保证了内存使用的高效与CPU的性能
过期删除策略引起的脏读现象
- 在单节点实例模式下,因为Redis是单线程模型,所以过期策略可以保证数据一致性。
- 在集群模式下,过期删除策略会引起脏读现象
数据的删除在主库执行,从库不会执行。对于惰性删除策略来说,3.2版本以前,从库读取数据时哪怕数据已过期还是会返回数据,3.2版本以后,则会返回空。
对于定期删除策略,由于只是随机抽取了一定的数据,此时已过期但未被命中删除的数据在从库中读取会出现脏读现象。
过期时间命令EX|PX,在主从同步时,因为同步需要时间,就会导致主从库实际过期时间出现偏差。比如主库设置过期时间60s,但同步全量花费了1分钟,那么在从库接收到命令并执行之后,就导致从库key的过期时间整体跨越了两分钟,而此时主库在一分钟之前数据就已经过期了。EXAT|PXAT 命令来设置过期时间节点。这样可避免增量同步的发生。但需注意主从服务器时间一致。
在实际使用过程中,过期时间配置只是一种常规手段,当key的数量在短时间内突增,就有可能导致内存不够用。此时就需要依赖于Redis内部提供的淘汰策略来进一步的保证服务的可用性。
结语
到这里,我们可得出一个结论:Redis的高性能不仅仅体现在单线程上,还在于内存和数据管理的相辅相成上。除此之外,Redis的多样化数据结构和vm体系也为其高性能提供了更加有力的支撑,后续我们可以一起研究学习。