Redis是一个内存键值对数据库,所以对于内存的管理尤为重要。Redis内部对于内存的管理主要包含两个方向,过期删除策略和数据淘汰策略。
1. 内存淘汰策略
1.1. 概述
内存淘汰策略指在Redis内存使用达到一定阈值的时候,执行某种策略释放内存空间,以便于接收新的数据。内存可使用空间由配置参数maxmemory决定(单位mb/GB)。故又叫"最大内存删除策略",也叫"缓存删除策略"。
1.2. 最大内存
# 客户端命令方式配置和查看内存大小 127.0.0.1:6379> config get maxmemory "maxmemory" "0" 127.0.0.1:6379> config set maxmemory 100mb OK 127.0.0.1:6379> config get maxmemory "maxmemory" "104857600" # 配置文件配置maxmemory大小 # WARNING: not setting maxmemory will cause Redis to terminate with an # out-of-memory exception if the heap limit is reached. # # NOTE: since Redis uses the system paging file to allocate the heap memory, # the Working Set memory usage showed by the Windows Task Manager or by other # tools such as ProcessExplorer will not always be accurate. For example, right # after a background save of the RDB or the AOF files, the working set value # may drop significantly. In order to check the correct amount of memory used # by the redis-server to store the data, use the INFO client command. The INFO # command shows only the memory used to store the redis data, not the extra # memory used by the Windows process for its own requirements. Th3 extra amount # of memory not reported by the INFO command can be calculated subtracting the # Peak Working Set reported by the Windows Task Manager and the used_memory_peak # reported by the INFO command. # maxmemory 100mb #...
若maxmemory=0则表示不做内存限制,但是对于windows系统来说,32位系统默认可使用空间是3G,因为整个系统内存是4G,需要留1G给系统运行。且淘汰策略会自动设置为noeviction,即不开启淘汰策略,当使用空间达到3G的时候,新的内存请求会报错。
1.3. 分类
淘汰策略配置maxmemory-policy,表示当内存达到maxmemory时,将执行配置的淘汰策略,由redis.c/freeMemoryIfNeeded 函数实现数据淘汰逻辑。
在redis文件目录下redis.conf中,可看到提供8种淘汰策略,在3.0之前提供其中6种,4.0以上版本增加了两种LFU策略。
#redis.conf文件配置方式 # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory # is reached. You can select one from the following behaviors: # # volatile-lru -> Evict using approximated LRU, only keys with an expire set. # allkeys-lru -> Evict any key using approximated LRU. # volatile-lfu -> Evict using approximated LFU, only keys with an expire set. # allkeys-lfu -> Evict any key using approximated LFU. # volatile-random -> Remove a random key having an expire set. # allkeys-random -> Remove a random key, any key. # volatile-ttl -> Remove the key with the nearest expire time (minor TTL) # noeviction -> Don't evict anything, just return an error on write operations. # # LRU means Least Recently Used # LFU means Least Frequently Used # # Both LRU, LFU and volatile-ttl are implemented using approximated # randomized algorithms. # The default is: # ... maxmemory-policy noeviction
数据淘汰策略 |
描述 |
noeviction |
不删除任何数据,当内存不足时直接报错 |
volatile-lru |
挑选最近最久未使用的数据进行淘汰 |
volatile-lfu |
挑选最近最少使用的数据进行淘汰 |
volatile-ttl |
挑选将要过期的数据进行淘汰 |
volatile-random |
随机选择数据进行淘汰 |
allkeys-lru |
挑选最近最久未使用的数据进行淘汰,包括所有数据 |
allkeys-lfu |
挑选最近使用次数最少的数据进行淘汰,包括所有数据 |
allkeys-random |
随机选择数据进行淘汰,包括所有数据 |
- LRU(Least Recently Used)最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。数据越久没被访问,优先淘汰。
- LFU(Least Frequently Used)最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。数据访问频次越少,优先淘汰。
- volatile:表示设置了带过期时间的key
- allkeys:表示所有的key
# 命令行配置方式 127.0.0.1:6379> CONFIG GET maxmemory-policy "maxmemory-policy" "noeviction" 127.0.0.1:6379> CONFIG SET maxmemory-policy volatile-lru OK 127.0.0.1:6379> CONFIG GET maxmemory-policy "maxmemory-policy" "volatile-lru"
1.4. 使用建议
- 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
- 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
- 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random 或 volatile-random,随机选择淘汰。
- 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。
1.5. 总结
Redis在实现淘汰策略时为了更合理的利用内存空间以及保证Redis的高性能,只是几近于算法的实现机制,其会从性能和可靠性层面做出一些平衡,故并不是完全可靠的。因此我们在实际使用过程中,建议都配置过期时间,主动删除那些不再使用的数据,以保证内存的高效使用。另外关于LRU和LFU算法,Redis内部在数据结构和实现机制上都做了一定程度的适应性改造
2. 过期删除策略
2.1. 概述
Redis中的数据过期删除策略是指在数据的有效时间到期后,如何从内存中删除这些数据的规则。
2.2. 常见的删除方式
- 定时删除:在写入key之后,根据否配置过期时间生成特定的定时器,定时器的执行时间就是具体的过期时间。用CPU性能换去内存存储空间——即用时间获取空间。
- 定期删除:提供一个固定频率的定时器,执行时扫描所有的key进行过期检查,满足条件的就进行删除。
- 惰性删除:数据不做及时释放,待下一次接收到读写请求时,先进行过期检查,若已过期则直接删除。用内存存储空间换取CPU性能——即用空间换取时间。
删除方式 |
优点 |
缺点 |
定时删除 |
能及时释放内存空间,不会产生滞留数据 |
频繁生成和销毁定时器,非常损耗CPU性能,影响响应时间和指令吞吐量 |
定期删除 |
固定频率进行过期检查,对CPU友好 |
1. 数据量大时,全局扫描损耗CPU性能,主线程阻塞导致其他请求响应延迟 2. 未能及时释放内存空间 3. 数据不一致 |
惰性删除 |
节约CPU性能 |
当某些数据长时间无请求访问时,会导致数据滞留,使内存无法释放,占用内存空间,甚至引发内存泄漏导致服务不可用 |
Redis中结合了定期删除与惰性删除两种方式来完成。下面针对两者展开描述。
2.3. 惰性删除
概述
惰性删除指的是设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
特点
- 优点 :对CPU友好,对于很多用不到的key不用浪费资源进行过期检查。
- 缺点 :对内存不友好,key过期不被使用,就不会被清理就会一直占用内存。
2.4. 定期删除
概述
每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键)。
特点
- 优点:相对于定时删除,可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除弥补惰性删除的缺点,能有效释放过期键占用的内存。
- 缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。
模式
- SLOW模式是定时任务,执行频率默认为10hz,执行间隔为100ms,执行时间不超过25ms。可通过修改配置文件redis.conf 的 hz 选项来调整这个次数。
- FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
这两种模式规定执行间隔以及执行时间,为了不影响主线程的执行。
2.5. 底层原理
当key设置了过期时间,Redis内部会将这个key带上过期时间放入过期字典(expires)中,当进行查询时,会先在过期字典中查询是否存在该键,若存在则与当前UNIX时间戳做对比来进行过期时间判定。
过期时间配置命令如下(即EX|PX|EXAT|PXAT):
# expire: t秒后过期 expire key seconds # pexpire: t毫秒后过期 pexpire key millseconds # expireat: 到达具体的时间戳时过期,精确到秒 expireat key timestamp # pexpireat: 到达具体的时间戳时过期,精确到毫秒 pexpire key millseconds
这四个命令看似有差异,但在RedisDb底层,最终都会转换成pexpireat指令。内部由db.c/expireGenericCommand函数实现,对外由上面四个指令调用
//expire命令 void expireCommand(redisClient *c) { expireGenericCommand(c,mstime(),UNIT_SECONDS); } //expireat命令 void expireatCommand(redisClient *c) { expireGenericCommand(c,0,UNIT_SECONDS); } //pexpire命令 void pexpireCommand(redisClient *c) { expireGenericCommand(c,mstime(),UNIT_MILLISECONDS); } //pexpireat命令 void pexpireatCommand(redisClient *c) { expireGenericCommand(c,0,UNIT_MILLISECONDS); } /* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT * and PEXPIREAT. Because the commad second argument may be relative or absolute * the "basetime" argument is used to signal what the base time is (either 0 * for *AT variants of the command, or the current time for relative expires). */ void expireGenericCommand(redisClient *c, long long basetime, int unit) { ... /* unix time in milliseconds when the key will expire. */ long long when; ... //如果是秒转换为毫秒 if (unit == UNIT_SECONDS) when *= 1000; when += basetime; ... }
- 过期字典内部存储结构:key表示一个指向具体键的指针,value是long类型的毫秒精度的UNIX时间戳。
- Rediskey过期时间内部流程图:
2.6. 脏读现象
在单节点实例模式下,因为Redis是单线程模型,所以过期策略可以保证数据一致性。
在集群模式下,过期删除策略会引起脏读现象
- 数据的删除在主库执行,从库不会执行。对于惰性删除策略来说,3.2版本以前,从库读取数据时哪怕数据已过期还是会返回数据,3.2版本以后,则会返回空。
- 对于定期删除策略,由于只是随机抽取了一定的数据,此时已过期但未被命中删除的数据在从库中读取会出现脏读现象。
- 过期时间命令EX|PX,在主从同步时,因为同步需要时间,就会导致主从库实际过期时间出现偏差。比如主库设置过期时间60s,但同步全量花费了1分钟,那么在从库接收到命令并执行之后,就导致从库key的过期时间整体跨越了两分钟,而此时主库在一分钟之前数据就已经过期了。EXAT|PXAT 命令来设置过期时间节点。这样可避免增量同步的发生。但需注意主从服务器时间一致。
2.7. 总结
Redis中惰性删除和定期删除这两种方式的结合,能很好的解决过期数据滞留内存的问题,同时也很好的保证了数据的一致性,保证了内存使用的高效与CPU的性能。
但在实际使用过程中,过期时间配置只是一种常规手段,当key的数量在短时间内突增,就有可能导致内存不够用。此时就需要依赖于Redis内部提供的淘汰策略来进一步的保证服务的可用性。