目录
一、Redis缓存穿透
1.1、缓存穿透原理
缓存穿透是指查询一个根本不存在的数据,比如一个商品表其中有商品ID:P001、P002、P003,在调用查询商品详情时传入商品ID:P004,P004在缓存中一定是不存在的会直接越过缓存层执行查询数据库逻辑,并且商品表中也是不存在的,查询不到数据则不会进行数据缓存。
- 造成缓存穿透的基本原因有两个:
- 自身业务代码或者数据出现问题
- 恶意攻击、 爬虫等造成大量空命中
1.2、缓存穿透代码演示
在下面示例代码中,当调用获取商品详情方法传入商品ID为P001-3时能获取到商品详情数据,并且会将数据库中查询到的商品详情插入Redis
中,下一次查询相同商品ID会直接读取Redis
中的数据而不会再去读取数据库,如果传入商品ID不为P001-3时,比如说P004,数据库中没有这个商品,数据库中查询不到则不会将P004缓存到Redis
中,那么每次查询P004都会执行查询数据库逻辑,这就是缓存穿透问题,流量没有被缓存处理还是会打到数据库。
@Service
public class ProductDetailsService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 商品详情key前缀
private final String PRODUCT_DETAILS_KEY_PREFIX = "PRODUCT_DETAILS_KEY:";
/**
* 获取商品详情V1
* @param productId 商品ID
*/
public Object getProductDetailsV1(String productId) {
if (StringUtils.isEmpty(productId)) {
throw new RuntimeException("商品ID不能为空");
}
// 缓存key
String productDetailsKey = PRODUCT_DETAILS_KEY_PREFIX + productId;
// 判断缓存是否存在,如果存在直接返回
boolean exist = redisTemplate.hasKey(productDetailsKey);
if (exist) {
Object value = redisTemplate.opsForValue().get(productDetailsKey);
System.out.println("获取到缓存数据 value=" + value);
return value;
}
// 如果缓存中没有获取到商品详情则查询数据库
Object productDetails = getDBProductDetails(productId);
// 如果查询数据库获取到了商品详情则将商品详情插入缓存
if (productDetails != null) {
redisTemplate.opsForValue().set(productDetailsKey, productDetails);
}
return productDetails;
}
/** 模拟查询数据库 */
private Object getDBProductDetails(String productId) {
switch (productId) {
case "P001":
return "商品P001-Java编程思想";
case "P002":
return "商品P002-Java并发编程的艺术";
case "P003":
return "商品P003-DDD领域驱动设计";
}
return null;
}
}
1.3、缓存穿透解决方案
解决方案一(数据库中查询不到数据也将key进行缓存)
在上面V1方法的基础上将数据库中查询不到数据返回的null
值也进行缓存,但是一定要设置一个短暂的过期时间,这样第二次查询就会被缓存拦截流量不会打到数据库,避免了部分场景的缓存穿透问题,但是无法很好的避免恶意攻击,恶意攻击时可以每次生成的请求商品详情ID都不同,如果将这些商品详情ID都存储到缓存中就算设置了过期时间,Redis
压力也会非常大,如果不考虑恶意攻击问题其实这个方案基本能解决缓存穿透问题。
public Object getProductDetailsV2(String productId) {
if (StringUtils.isEmpty(productId)) {
throw new RuntimeException("商品ID不能为空");
}
// 缓存key
String productDetailsKey = PRODUCT_DETAILS_KEY_PREFIX + productId;
// 判断缓存是否存在,如果存在直接返回
boolean exist = redisTemplate.hasKey(productDetailsKey);
if (exist) {
Object value = redisTemplate.opsForValue().get(productDetailsKey);
System.out.println("获取到缓存数据 value=" + value);
return value;
}
// 如果缓存中没有获取到商品详情则查询数据库
Object productDetails = getDBProductDetails(productId);
// 如果查询数据库获取到了商品详情则将商品详情插入缓存
if (productDetails != null) {
redisTemplate.opsForValue().set(productDetailsKey, productDetails);
}else {
// 如果数据库查询不到数据也进行缓存,并且设置一个过期时间
redisTemplate.opsForValue().set(productDetailsKey, null,1, TimeUnit.MINUTES);
}
return productDetails;
}
解决方案二(使用布隆过滤器)
对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用很少。
Redisson提供了布隆过滤器的实现可以直接使用。
@Autowired
private RedissonClient redissonClient;
public Object getProductDetailsV3(String productId) {
if (StringUtils.isEmpty(productId)) {
throw new RuntimeException("商品ID不能为空");
}
// 判断布隆过滤器中是否有我们的商品详情ID,如果没有则代表系统没有这个商品详情信息
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(PRODUCT_DETAILS_BLOOM_FILTER_KEY_PREFIX);
if (!bloomFilter.contains(productId)) {
System.out.println("通过布隆过滤器判断商品详情不存在 productId=" + productId);
return null;
}
// 缓存key
String productDetailsKey = PRODUCT_DETAILS_KEY_PREFIX + productId;
// 判断缓存是否存在,如果存在直接返回
boolean exist = redisTemplate.hasKey(productDetailsKey);
if (exist) {
Object value = redisTemplate.opsForValue().get(productDetailsKey);
System.out.println("获取到缓存数据 value=" + value);
return value;
}
// 如果缓存中没有获取到商品详情则查询数据库
Object productDetails = getDBProductDetails(productId);
// 如果查询数据库获取到了商品详情则将商品详情插入缓存
if (productDetails != null) {
redisTemplate.opsForValue().set(productDetailsKey, productDetails);
}
return productDetails;
}
/**
* 初始化布隆过滤器
* 使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放
* 布隆过滤器只能新增数据不能删除数据,如果要删除得重新初始化数据
*/
private void initProductDetailsBloomFilter() {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(PRODUCT_DETAILS_BLOOM_FILTER_KEY_PREFIX);
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L, 0.03);
//将商品详情ID P001-3 插入到布隆过滤器中
bloomFilter.add("P001");
bloomFilter.add("P002");
bloomFilter.add("P003");
}
二、Redis缓存击穿(缓存失效)
由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。
三、Redis缓存雪崩
3.1、缓存雪崩原理
缓存雪崩指的是缓存层支撑不住或宕掉后, 流量会全部打向后端存储层。由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。
3.2、缓存雪崩解决方法
- 1、保证缓存层服务高可用,比如使用Redis Sentinel或Redis Cluster。
- 2、做好高并发测试,确保现有架构能抗住线上并发。
- 3、使用Sentinel或Hystrix服务保护组件对业务接口做限流熔断降级处理。