概述
本节学习下Redis作为缓存时,几种常见的情形及其解决解决方案
1. 缓存淘汰
1. 缓存写满:
为了保证较高的性价比,缓存的空间容量必然要小于后端数据库的数据总量。但是内存大小毕竟有限,随着要缓存的数据量越来越大,有限的缓存空间不可避免地会被写满;
2. 缓存污染:
留存在缓存中的数据,实际不会被再次访问了,但是又占据了缓存空间,这种数据量比较大甚至会占满缓存;
缓存写满后就会涉及缓存淘汰的问题。
1.1 淘汰策略
Redis4.0以后共8种淘汰策略:
1.
:在设置了过期时间的数据中进行淘汰
volatile-random
: 随机
volatile-ttl
: 过期时间的先后
volatile-lru
: 最近最少使用
volatile-lfu
: 最近使用次数最少
2.
:在所有数据中进行淘汰
allkeys-lru
: 最近最少使用
allkeys-random
: 随机
allkeys-lfu
: 最近使用次数最少
3.
:不进行淘汰
noeviction
: 不进行淘汰
1.2 LRU策略
传统LRU实现方式使用链表实现(访问后就移动到队头,从队尾直接淘汰),需要额外的空间开销,同时移动节点也会消耗性能;
1.2.1 Redis对LRU进行了优化:
1.
在RedisObject中的lru字段记录最近一次访问的时间戳
2.
决定淘汰数据时,第一次会随机选出N个数据作为一个候选集合,把lru字段值最小的数据淘汰出去
3.
准备好候选集等待被删除,即挑选lru字段值小于候选集合中最小的lru值的数据放进候选集
4.
决定淘汰数据时,直接从候选集中淘汰lru字段小的数据
1.2.2 Mysql BufferPool对LRU进行了优化:
lru.png在 InnoDB 实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。图中 LRU_old 指向的就是 old 区域的第一个位置,是整个链表的 5/8 处。也就是说,靠近链表头部的 5/8 是 young 区域,靠近链表尾部的 3/8 是 old 区域。
改进后的 LRU 算法执行流程变成了下面这样。
1.
图中状态 1,要访问数据页 P3,由于 P3 在 young 区域,因此和优化前的 LRU 算法一样,将其移到链表头部,变成状态 2。2.
之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉数据页 Pm,但是新插入的数据页 Px,是放在 LRU_old 处。3.
处于 old 区域的数据页,每次被访问的时候都要做下面这个判断:若这个数据页在 LRU 链表中存在的时间超过了 1 秒,就把它移动到链表头部;如果这个数据页在 LRU 链表中存在的时间短于 1 秒,位置保持不变。1 秒这个时间,是由参数 innodb_old_blocks_time 控制的。其默认值是 1000,单位毫秒。
这个策略,就是为了处理类似全表扫描的操作量身定制的。以扫描 200G 的历史数据表为例,改进后的 LRU 算法的操作逻辑:
1.
扫描过程中,需要新插入的数据页,都被放到 old 区域 ;
2.
一个数据页里面有多条记录,这个数据页会被多次访问到,但由于是顺序扫描,这个数据页第一次被访问和最后一次被访问的时间间隔不会超过 1 秒,因此还是会被保留在old 区域;
3.
再继续扫描后续的数据,之前的这个数据页之后也不会再被访问到,于是始终没有机会移到链表头部(也就是 young 区域),很快就会被淘汰出去。
可以看到,这个策略最大的收益,就是在扫描这个大表的过程中,虽然也用到了 BufferPool,但是对 young 区域完全没有影响,从而保证了 Buffer Pool 响应正常业务的查询命中率。
1.2.3 LRU存在的问题
无法解决在处理扫描式(例如范围查询或keys *等)单次查询操作时的缓存污染问题
1.2.4 使用建议
1.
优先使用allkeys-lru策略
2.
如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用allkeys-random策略,随机选择淘汰的数据就行
3.
如果业务中有置顶的需求(置顶新闻、置顶视频等),可以使用volatile-lru策略,同时不给这些置顶数据设置过期时间。这样这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据LRU规则进行筛选
1.3 LFU策略
LFU缓存策略是在LRU策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
1.3.1 对LRU做的优化
1.
把原来24bit大小的lru字段拆成,前16bit数据的访问时间戳(分钟为单位),后8bit数据的访问次数(8位最多到255)
2.
淘汰数据时从候选集中先选访问次数最少的,访问次数相等时再选时间戳最小的
3.
为了解决访问次数过大的问题,redis采用非现线递增方式
(递增频率通过lfu_log_factor
控制),并不是每访问一次就加1
4.
为了解决有些数据短时间内被多次访问,但是后续不再被访问的场景,redis采用衰减机制
(通过lfu_decay_time
控制访问次数衰减幅度)
1.3.2 使用建议
1.
LRU策略更加关注数据的时效性,而LFU策略更加关注数据的访问频次。通常情况下实际应用的负载具有较好的时间局部性,所以LRU策略的应用会更加广泛。但是,在扫描式查询的应用场景比较多时,建议优先使用LFU策略,可以很好地应对缓存污染问题;
2. 缓存过期
缓存过期后数据会被删除
2.1 删除策略
-
惰性删除
: 当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据 -
定时删除
: 每隔一段时间(默认100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除
2.2 过期方案
1.
从库不会删除数据,3.2版本以后访问到从库过期数据,从库返回空
2.
使用EXPIREAT/PEXPIREAT命令设置过期时间,避免从库上的数据过期时间滞后
3.
slave-read-only用来控制 slave 是否可写,4.0版本后yes时本身就写在从库的带有过期时间的数据会被从库清理,从主库同步过来的带有过期时间的数据从库不会清理
-------------over------------
网友评论