系统中常常用Redis来做缓存,极大的提升了系统性能和效率,但同时也存在一些问题。其一是数据一致性问题,严格意义上来讲,只要用到缓存,那就会有一致性问题,这是无解的。另一个问题就是本文要讲的缓存穿透、缓存击穿、缓存雪崩,不仅仅局限于Redis,其他方式实现的缓存,也存在着三个问题。
一、缓存穿透
缓存穿透是指,用户在查询一个数据库肯定不存在的数据时,这时的返回结果的null,结果不会存入缓存。假设用户不断的发起这种请求,始终不会命中缓存,从而导致所有查询都落到数据库上,造成数据库压力过大。
问题代码:
public Object getOrder(Long orderId) {
Object orderInfo = redisTemplate.opsForValue().get(String.valueOf(orderId));
if (orderInfo != null) { // 命中缓存,则直接返回redis中的值
return orderInfo;
}
// 未命中缓存,则查询数据库
orderInfo = orderDao.selectByOrderId(orderId);
if (orderInfo != null) { // 查到了结果,则放入缓存
redisTemplate.opsForValue().set(String.valueOf(orderId), orderInfo);
}
return orderInfo;
}
试想一下,如果黑客发起请求,参数orderId = -1,这种数据肯定是不会存在的,每次都会走到查询数据库,并且数据的查询结果也是null,也不会去缓存这个结果。
解决方案:
1、用户鉴权、参数校验等,将明显不合理的请求,拦截在上层
2、数据库查询结果为null时,也进行缓存,只不过缓存有效期设短一点,以免影响正常数据的缓存
解决代码:
public Object getOrder(Long orderId) {
Object orderInfo = redisTemplate.opsForValue().get(String.valueOf(orderId));
if (orderInfo != null) { // 命中缓存,则直接返回redis中的值
return orderInfo;
}
// 未命中缓存,则查询数据库
orderInfo = orderDao.selectByOrderId(orderId);
if (orderInfo != null) { // 查到了结果,则放入缓存,60分钟有效期
redisTemplate.opsForValue()
.set(String.valueOf(orderId), orderInfo, 60, TimeUnit.MINUTES);
} else { // 否则,也放入缓存,60秒有效期
redisTemplate.opsForValue().set(String.valueOf(orderId), null, 60, TimeUnit.SECONDS);
}
return orderInfo;
}
二、缓存击穿
缓存击穿是指,某一个热点key数据过期时,多个线程高并发的去请求这个key,这是缓存刚好过期,到时所有的并发请求全部去数据库查询数据。
解决方案:
其实大多数实际业务场景中,即时发生缓存击穿,也不会对数据库造成太大的压力,因为一般的公司业务,并发量不会那么高,即时很不幸,你们发生了这种情况,可以通过将这些热点key,设置成永不过期即可。另外一种方式是,通过加锁来控制查询数据库的线程访问
三、缓存雪崩
缓存雪崩是指某一个时间,缓存中大量的key到了过期时间,这时候如果发生大量查询的话,将全部落到数据库上。与缓存击穿不同,前者是因为某一个key过期,并发查询这个key,后者是一个大量key同时过期,然后很多查询都查不到缓存。
解决方案:
1、缓存数据的过期时间,设置成一个随机值,防止同一时间大量key过期,热点数据有效期长一些,冷门数据有效期短一些
2、某些热点的key,设置成永不过期
3、如果是因为缓存服务器宕机造成的雪崩,可以采用分布式缓存服务器,提高其可用性。