本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
Redis作为一种常见的非关系型数据库,因其优异的性能,在分布式系统中具备广泛的应用,通常使用Redis来实现缓存功能,基本思路如下所示:
Redis作为缓存在项目中的应用极大的提升了程序的性能和效率,尤其是查询数据时。但凡事有利皆有弊,Redis缓存提高程序并发性能的同时,也带来了一些问题,如常见的缓存穿透、缓存击穿以及缓存雪崩等。
缓存穿透
何为缓存穿透?即请求查询一个在数据库中也不存在的数据,导致所有的请求避开了缓存,流量直接打到数据库层。一旦当这种请求量激增时,数据库压力骤增,严重时可能会导致数据库直接挂掉。
解决办法:
- 接口层增加校验,如用户鉴权校验,请求数据的
id进行基础校验,id<0这种情况的直接拦截掉; - 若是缓存或者是数据库中都不存在该数据,这时可以将这个
key在Redis中对应的缓存设置为一个null值,但是缓存有效时间设置短一点,如30s左右,以防止这个键在后面某一时刻真的有值; - 布隆过滤器:
Bloomfilter类似于一个Java中的Set集合,可用于快速判断某个元素是否在集合中,一个典型应用场景就是判读一个key是否存在于某个容器中,不存在就直接返回。布隆过滤器的关键在于hash算法和容器大小。
可能存在的问题:
- 需要更多的键:遭受恶意攻击时,可能每次请求的
key都不一样,那就会将这些key都写入到缓存当中,虽然只是一个null值,但如果数据量较大时,对于Redis的性能还是有一定影响的,所以一般在设置null值的同时也要设置过期时间; Redis缓存层与DB层之间会存在短暂的数据不一致现象:因为cache层设置了过期时间,如果某一时刻,DB层存在了某key值对应的数据,但缓存层中该key对应的值仍为null,只有等过期时间过去之后,重新从DB层查询数据刷新缓存才能保持一致。
缓存击穿
何为缓存击穿?就是在某个时刻,大量请求同时查询同一个key时,这个key对应的缓存正好失效了,导致这些请求全部打到了DB上,此时DB有可能挂掉。
解决办法:
- 加互斥锁,保证某一时刻只能有一个线程去进行缓存更新操作,其他线程全部等待缓存更新完成
- 设置热点数据永不过期
互斥锁
当从缓存中获取不到数据时,先加锁,然后从数据库中查询数据,若查到则刷新缓存,最后释放锁。如果其他线程获取锁失败,则休眠一段时间后重试。下面是使用Redis的setNx来实现分布式锁,保证只有一个线程去进行缓存更新操作。
public String get(String key) throws Exception{
String value=redisTemplate.get(key);
if(StringUtils.isBlank(value)){
String mutexKey="mutex:key:"+key;
if(redisTemplate.setNx(mutexKey,"1")){
value=getFromDB(key);
if(StringUtils.isNotBlank(value)){
redisTemplate.setEx(key,value,5*60);
}
//释放锁
redisTemplate.del(mutexKey);
}else{
//其他线程休息1000ms后重试
Thread.sleep(1000);
get(key);
}
}
return value;
}
缓存永不过期
缓存永不过期的意思是,真正的缓存过期时间不由Redis进行控制,而是由程序代码控制。当获取数据时,发现获取数据超时,就需要发起一个异步请求去加载数据。这种策略的优点是不会产生死锁,但有可能造成缓存不一致的现象,但一般情况下是适用的。
public String get(final String key){
V v=redisTemplate.get(key);
String value=v.getValue();
long logicTimeOut=v.getLogicTimeOut();
if(logicTimeOut>=System.currentTimeMillis()){
threadPool.execute(()-{
String mutexKey="mutex:key:"+key;
if(redisTemplate.setNx(mutexKey,"1")){
vaue=getFromDB(key);
if(StringUtils.isNotBlank(value)){
redisTemplate.setEx(key,value,5*60);
}
redisTemplate.del(mutexKey);
}
});
}
return value;
}
缓存雪崩
缓存雪崩是指缓存中大量的key设置了相同的过期时间,导致在某个时刻这些缓存全部失效,然后所有的请求直接全部打到DB层上。与缓存击穿不同的是,击穿针对的是高并发下查询同一个key,而雪崩是高并发下不同的key都过期了,请求全部打到数据库上。
解决方法:
- 不同
key的过期时间设置为随机值,这样可以避免同一时间大量key过期 - 如果缓存是分布式部署,则将这些热点数据均匀分布到不同的缓存数据库中
- 设置热点数据永不过期
总结
简单介绍了Redis作为缓存使用时可能出现的问题,如缓存穿透、缓存击穿和缓存雪崩。在工作中使用需要注意这几个问题,保证服务的高可用。