直奔主题,Redis
是部署在某个机器上的,而内存是有限的,数据却可以是无限的,那么当Redis
中的数据太多了,该怎么办?这就用上了Redis
的缓存淘汰机制。
Redis
中的缓存淘汰策略主要可以分为两大类:
Redis3.0
版本之后):noeviction
。当Redis
使用的内存超过了maxmemory
值时,并不会淘汰数据。此时Redis
不再提供对外服务,而是直接返回错误。会进行数据淘汰策略又可以分为两大类:
针对设置了过期时间的数据进行淘汰:4种
volatile-random
:在设置了过期时间的键值对中,随机进行删除。volatile-ttl
:在设置了过期时间的键值对中,越早过期的数据优先被删除。volatile-lru
:在设置了过期时间的键值对中,采用LRU
算法进行删除(最近最少使用(最长时间)淘汰算法)。volatile-lfu
:在设置了过期时间的键值对中,采用LFU
算法进行删除(最不经常使用(最少次)淘汰算法)。针对所有数据进行淘汰:3种
allkeys-lru
:使用 LRU
算法在所有数据中进行筛选。allkeys-random
:从所有键值对中随机选择并删除数据allkeys-lfu
:使用 LFU
算法在所有数据中进行筛选。LRU
算法(Least Recently Used
):这是按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来被淘汰,如图:
传统的LRU
算法有着一定的缺陷:
LRU
在数据更新的时候会造成链表移动操作。这个过程很耗时,会影响 Redis
缓存性能。Redis
对LRU
做了一定的优化:
RedisObject.lru
字段。N
个数据,作为一个候选集合S
。N
个数据的lru
字段,将lru
字段值最小的数据从缓存中淘汰。N
这个参数可以通过maxmemory-samples
来配置:S
中。(保持集合S
中元素总量不变)同时要求能够进入的数据必须满足这样的条件:其lru
字段必须小于候选集合S
中最小的lru
值。LRU
算法中,对于链表的频繁维护操作。对于Redis
的缓存淘汰机制,有几个建议:
allkeys-lru
策略。把最近最常访问的数据留在缓存中,提升应用的访问性能。尤其是那些有明显的冷热数据划分的应用缓存。allkeys-random
策略,随机淘汰。需要值得注意的一点是:Redis
通过淘汰机制将需要淘汰的数据进行删除的时候,无论这个数据在当前状态下是否是干净的,它都会删除。
Mysql
)中的数据保持一致。因此在使用Redis
作为缓存的时候,在更新缓存的时候,倘若数据库中也有这个缓存字段,那么需要同时修改数据库中的值。也就是所谓的保持数据一致性。
在第一章节我提到了数据的一致性。但是,往往在实际生产中,这样一个说起来简单的操作,却是代码编写上一个很大的难题:如何保证缓存和后端数据库的一致性问题?
除此之外,比较常见的缓存问题还有:缓存雪崩、缓存击穿、缓存穿透。
首先什么是缓存和数据库数据的数据一致性?满足以下条件:
如果不满足上述条件,就是所谓的缓存不一致性了。对于这个问题,常见的有三种解决方案:
Cache Aside
。Read/Write Throught
。Write Back
。首先粗略的来说下三种策略。Cache Aside
策略:即缓存只用来读。(最常用也是最容易实现的策略)
Read/Write Throught
策略:读写操作都只操作缓存。
Write Back
策略:读写缓存 + 异步写回。
Page Cache
中。接下来具体说下一致性问题,首先需要明确几点:
Redis
作为缓存的目的是为了快,那么在解决缓存一致性问题的时候,难免会加大系统的复杂度,性能和一致性不可能两全其美,只能做到平衡。Cache Aside
策略就可以了。代码的业务逻辑也非常的简单。而且,正常情况下(不宕机,没有夸张的网络延迟),非常普通的(没有高并发)那种业务发生这种数据不一致性的可能性也太低了。当然作为一名合格的程序员,需要防患于未然。如果需要寻求缓存一致性,难以避免的是:在对缓存或者数据库里的数据进行删除或者修改操作的时候。肯定需要同时对缓存和数据库里的数据进行操作,也就是有两个步骤。 那么就有一个顺序问题:是先操作数据库还是缓存?
回答:
要想保证数据的一致性,在上述基础上,我们先更新数据库,再删除缓存。同时利用消息队列来保证重试。 流程如下:
假设有个主线程A
:需要对某个Key
进行修改。
key
加入到第三方消息队列中。然后通过另起一个线程B
,去不断地尝试删除它,直到成功。(可以限制一下重试的次数,避免无限死循环)Key
做幂等性处理。保证这个Key
只会被消费一次。例如:
public void updateData(String key, Object data){
// 1.先更新数据库
updateMysqlData(data);
if(!redis.del(key)){
// 2.如果失败了,放到队列中,去不断重试。
mq.send(key);
new Thread(()->asyncDel()).start();
}
}
public void asyncDel(){
int count = 0;
String key = mq.get();
// 循环调用删除
while(!redis.delKey(key)) {
count++;
// 设定个阈值,避免死循环
if (count > 5) {
throw new MyException();
}
}
// 删除完毕,不管是否成功,记得把队列中的key移除
mq.remove(key);
}
至于其他的什么延时双删的解决方案,可以自行百度。再者,你可以给文章中的两个操作加上事务,但是这样会造成性能的下降,可能会适得其反。
除了用消息队列做重试机制以外,也可以订阅数据库的变更日志,在操作缓存(阿里的canal
,仅供参考)。
缓存雪崩:短时间内大量缓存失效,导致请求直接访问数据库。数据库压力剧增。
其发生的原因主要是:缓存中有大量的数据同时过期。那么我们只需要给不同的key
设置不同的过期时间即可。
除此之外,在业务逻辑上我们可以通过服务降级的方式来解决,服务降级根据数据的不同采取不一样的处理方式:
同时可以通过服务限流,控制一段时间内的最高请求数量,将数据库需要处理的请求控制在一定范围内。
缓存击穿:针对某个访问非常频繁的热点数据的请求,缓存中不存在,从而导致请求发送到了后端数据库,导致了数据库压力激增。
和缓存雪崩比较相似,两者都是Redis
缓存中没有对应的数据而导致请求直接流入数据库,导致其压力剧增。只不过具体的表现形式有所不同而已。
key
的过期时间,避免key
在同一时间大量过期,可以给的过期时间加上一定的随机数。缓存穿透:要访问的数据既不在Redis
中,也不在数据库中。一般是攻击请求,即发起大量的查询请求,而查询的数据本身不存在。导致同时给Redis
和数据库造成巨大压力。
首先,针对一些特定的恶意攻击请求,我们最好在请求入口处加上参数的校验,避免恶意攻击。我们甚至可以对一些恶意攻击的IP
进行封锁。
另一方面,这里我们可以利用Redis
中的布隆过滤器,它的工作机制:
bit
数组和 N
个哈希函数组成,可以用来快速判断某个数据是否存在。N
个哈希函数,分别计算这个数据的哈希值,得到对应的N
个哈希值。N
个哈希值对bit
数组的长度进行取模,得到每个哈希值在数组中的对应位置。bit
值设置为1即可。这样,我们就可以在插入数据的时候,将这个数据也放进去,然后每次查询的时候用布隆过滤器做一次校验即可。不过布隆过滤器有这么几个点需要引起注意:
bit
上。即哈希冲突。key
是否存在可能有误判。但是对于某个key
不存在,却是能准确地计算出来的。简单总结下就是:
key
设置不同的过期时间。服务降级、服务限流。