Redis缓存过期淘汰策略
Redis内存满了怎么办
Redis内存配置
1)查看Redis最大占用内存
打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。
2)redis默认内存多少可以用?
如果不设置最大内存或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。
3)一般生产上如何配置?
一般推荐Redis设置内存为最大物理内存的四分之三,也就是0.75。
4)如何修改redis内存设置
- 通过修改文件配置
maxmemory 104857600
- 通过命令修改
config set maxmemory 104857600
5)什么命令查看redis内存使用情况?
info memory
Redis内存使用超出了设置值会发生什么
改改配置,故意把最大值设为1个byte试试。
出现以下报错: (error) OOM command not allowed when used memory > 'maxmemory'
结论:
设置了maxmemory的选项,假如redis内存使用达到上限,没有加上过期时间就会导致数据写满maxmemory 为了避免类似情况,引出下一章内存淘汰策略。
过期键的删除策略
如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?
不是,那过期后到底什么时候被删除的?是个什么操作?
数据删除策略的目标:在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕机或 内存泄露。
定时删除
对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作。立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。
- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用。
- 缺点:但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙不过来啦😠。这会产生大量的性能消耗,同时也会影响数据的读取操作。
惰性删除
对内存不友好,用存储空间换取处理器性能(拿空间换时间)
数据到达过期时间,不做处理。等下次访问该数据时,如果未过期则返回数据;发现已过期则删除并返回不存在。
- 优点:节约CPU性能,发现必须删除的时候才删除
- 缺点:内存压力很大,出现长期占用内存的数据
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏,无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。
具体实现
过期键的惰性删除策略由expireIfNeeded()函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
- 如果输入键已经过期,那么将输入键从数据库中删除
- 如果输入键未过期,那么不做任何处理
对应源码 db.c/expireIfNeeded 方法:
int expireIfNeeded(redisDb *db, robj *key) {
// 键未过期返回0
if (!keyIsExpired(db,key)) return 0;
// 如果运行在从节点上,直接返回1,因为从节点不执行删除操作,可以看下面的复制部分
if (server.masterhost != NULL) return 1;
// 运行到这里,表示键带有过期时间且运行在主节点上
// 删除过期键个数
server.stat_expiredkeys++;
// 向从节点和AOF文件传播过期信息
propagateExpire(db,key,server.lazyfree_lazy_expire);
// 发送事件通知
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
// 根据配置(默认是同步删除)判断是否采用惰性删除(这里的惰性删除是指采用后台线程处理删除操做,这样会减少卡顿)
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
折中方案:定期删除
周期性抽查存储空间(随机抽查,重点抽查)
定期删除策略是前两种策略的折中:
定期策略是每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU 时间的影响,同时也减少了内存浪费:
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除操作的执行时长和频率:
- CPU性能占用设置有峰值,检测频度可自定义设置特点
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
举例:
redis默认每个100ms检查,是否有过期的key,有过期key则删除。⚠️注意:redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除(存在漏网之🐟)。
具体实现
Redis 默认会每秒进行 10 次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典(保存了数据库中所有键的过期时间)中的所有键,而是采用了如下方法:
- 从过期字典中随机取出 20 个键
- 删除这 20 个键中过期的键
- 如果过期键的比例超过 25% ,重复步骤 1 和 2。不超过则检査下ー个库的过期键,0-15循环。另外期间如果达到了扫描时间上限25ms,则会记录下当前扫描的库,下次从该库开始扫描。
- 为了保证扫描不会出现循环过度,导致线程卡死现象,还增加了扫描时间的上限,默认是 25 毫秒(即默认在慢模式下,如果是快模式,扫描上限是 1 毫秒)。
对应源码 expire.c/activeExpireCycle 方法:
void activeExpireCycle(int type) {
...
do {
...
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
// 选过期键的数量,为 20
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
while (num--) {
dictEntry *de;
long long ttl;
// 随机选 20 个过期键
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
...
// 尝试删除过期键
if (activeExpireCycleTryExpire(db,de,now)) expired++;
...
}
...
// 只有过期键比例 < 25% 才跳出循环
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
...
}
补充
因为 Redis 在扫描过期键时,一般会循环扫描多次,如果请求进来,且正好服务器正在进行过期键扫描,那么需要等待 25 毫秒,如果客户端设置的超时时间小于 25 毫秒,那就会导致链接因为超时而关闭,就会造成异常,这些现象还不能从慢查询日志中查询到,因为慢查询只记录逻辑处理过程,不包括等待时间。
所以我们在设置过期时间时,一定要避免同时大批量键过期的现象,所以如果有这种情况,最好给过期时间加个随机范围,缓解大量键同时过期,造成客户端等待超时的现象。
Redis采用的过期策略
Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
RDB、AOF和复制功能对过期键的处理
RDB
生成 RDB 文件:
- 在执行 save 命令或 bgsave 命令创建一个新的 RDB文件时,程序会对数据库中的键进行检查,已过期的键就不会被保存到新创建的 RDB文件中
载入 RDB 文件:
-
主服务器:载入 RDB 文件时,会对键进行检查,过期的键会被忽略
-
从服务器:载入 RDB文件时,所有键都会载入。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以过期的键载入也不会造成啥影响。
AOF
AOF 文件写入
- 当过期键被惰性删除或定期删除后,程序会向 AOF 文件追加一条 del 命令,来显示的记录该键已经被删除。
AOF 重写
- 和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
主从复制
从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键后,会显示地向所有从服务器发送一个 del 命令,告知从服务器删除这个过期键
- 从服务器收到在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,只有在收到主服务器的 del 命令后,才会删除,这样就能保证主从服务器的数据一致性。
🤔️ redis集群,开启了 读写分离 写操作全在主,读操作全在从。过期的key 全靠redis master定期扫描到 才能删除? 这样会产生极大的资源浪费吧。
如果主从服务器链接断开怎么办?
Redis 采用 PSYNC 命令来执行复制时的同步操作,当从服务器在断开后重新连接主服务器时,主服务器会把从服务器断线期间执行的写命令发送给从服务器,然后从服务器接收并执行这些写命令,这样主从服务器就会达到一致性,那主服务器如何判断从服务器断开链接的过程需要哪些命令?主服务器会维护一个固定长度的先进先出的队列,即复制积压缓冲区,缓冲区中保存着主服务器的写命令和命令对应的偏移量,在主服务器给从服务器传播命令时,同时也会往复制积压缓冲区中写命令。从服务器在向主服务器发送 PSYNC 命令时,同时会带上它的最新写命令的偏移量,这样主服务器通过对比偏移量,就可以知道从服务器从哪里断开的了
如果发生网络抖动,主服务器发送的 del 命令没有传递到从服务器怎么办?
其实主从服务器之间会有心跳检测机制,主从服务器通过发送和接收 REPLCONF ACK 命令来检查两者之间的网络连接是否正常。当从服务器向主服务器发送 REPLCONF ACK 命令时,主服务器会对比自己的偏移量和从服务器发过来的偏移量,如果从服务器的偏移量小于自己的偏移量,主服务器会从复制积压缓冲区中找到从服务器缺少的数据,并将数据发送给从服务器,这样就达到了数据一致性
Redis删除策略总结 🤔
说说redis的过期键删除策略
- Redis采用的是惰性删除和定期删除两种策略,通过配合使用这两种策略,可以合理的平衡CPU使用时间和内存空间。惰性删除指的是数据如果到达了过期时间并不会马上删除,而是直到下次访问的时候再通过调用expireIfNeeded()方法来检查输入键是否过期,过期则进行删除操作,这种策略对CPU比较友好,但是比较浪费内存空间。定期删除指的是每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU 时间的影响,同时也减少了内存浪费。具体是通过activeExpireCycle ()方法来实现,这个方法的主要逻辑是Redis 默认会每秒进行 10 次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典中的所有键,而是从过期字典中随机取出 20 个键并删除这 20 个键中过期的键,如果过期键的比例超过 25% ,重复以上步骤;不超过则检査下ー个库的过期键,0-15循环。另外期间如果达到了扫描时间上限25ms,则会记录下当前扫描的库,下次从该库开始扫描。
Redis缓存淘汰策略
上述删除策略存在的问题
- 定期删除时,从来没有被抽查到
- 惰性删除时,也从来没有被点中使用过
从而导致大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽。必须要有一个更好的兜底方案。
缓存淘汰策略种类
- no-eviction: 不会驱逐任何key 😎出厂默认
- volatile-ttl: 删除马上要过期的key
- allkeys-lru: 对所有key使用LRU算法进行删除 😎 最通用的方案
- volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除
- allkeys-random: 对所有key随机删除
- volatile-random: 对所有设置了过期时间的key随机删除
- allkeys-lfu: 对所有key使用LFU算法进行删除
- volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除
总结
一共有2(维度)*4(方面)=8
种方案:
2个维度:
- 过期键种筛选
- 所有键中筛选
4个方面:
- LRU
- LFU
- random
- ttl
如何配置和修改
- 命令方式
config set maxmemory-policy allkeys-lru
- 配置文件中添加
maxmemory-policy allkeys-lru
Redis的LRU算法简介
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。
设计思想:
-
所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)
-
特性要求分析
- 必须有顺序之分,以区分最近使用的和很久没用到的数据排序。
- 写和读操作一次搞定。
- 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头)
查找快,插入快,删除快,且还需要先后排序——–>什么样的数据结构满足这个问题?
你是否可以在O(1)时间复杂度内完成这两种操作?如果一次就可以找到,你觉得什么数据结构最合适?
- LRU的算法核心是哈希链表,本质就是HashMap+DoubleLinkedList 时间复杂度是O(1),哈希表+双向链表的结合体。
- 或者直接使用JDK中的 LinkedHashMap 类。
既已览卷至此,何不品评一二: