实战中有效应对缓存穿透、击穿、雪崩的综合解决方案

缓存三兄弟

缓存穿透

什么是缓存穿透

请求非法 Key,缓存和数据库都无,请求都到了数据库中(穿透了缓存和数据库)

在这里插入图片描述

解决方法

1)缓存无效 Key:如果缓存和数据库都查不到某个 Key 的数据就写一个到 Redis 中去并设置一个短暂的 ttl。

2)布隆过滤器:过滤无效 Key,不让请求打到数据库中。

3)接口限流:根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制。

缓存击穿

什么是缓存击穿

热点 Key 过期,在数据库存在,在缓存中不存在(击穿了缓存)。

在这里插入图片描述

解决方案

1)热点数据永不过期:对秒杀、抢购等已知热点数据,ttl 设置为-1。

2)缓存预热:使用 HotKey 等手段,探测热点数据库,针对热点数据提前放入缓存。

3)加锁:保证只有一个请求去访问数据库和重建缓存。

缓存雪崩

什么是缓存雪崩

缓存在同一时间大面积的失效(像雪崩一样)。

在这里插入图片描述

解决方案

1)设置随机过期时间:在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时过期,从而减少缓存雪崩的风险。

2)缓存预热:针对热点数据提前预热,将其存入缓存中并设置合理的过期时间。

实战缓存穿透、击穿、雪崩

1. 引入 Redisson 依赖并配置

1)引入 Redis 和 Redisson 的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <verison>3.21.3</verison>
</dependency>

2)在配置文件application.yml|yaml|properties中配置redis相关参数

# yml
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: # 有就写

2. 配置布隆过滤器

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 布隆过滤器配置
 * 错误率越低,位数组越长,布隆过滤器的内存占用越大
 * 错误率越低,散列 Hash 函数越多,计算耗时较长
 */
@Configuration(value = "rBloomFilterConfiguration")
public class RBloomFilterConfiguration {

    /**
     * 防止用户注册查询数据库的布隆过滤器
     */
    @Bean
    public RBloomFilter<String> myBloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("rCachePenetrationBloomFilter");
        // expectedInsertions:预估布隆过滤器存储的元素长度 = 1亿
		// falseProbability:运行的误判率 = 设置为千分之一的误判率
        cachePenetrationBloomFilter.tryInit(100000000L, 0.001);
        return cachePenetrationBloomFilter;
    }
}

3. 当缓存不存在时,查询布隆过滤器

没有命中缓存,这时先去布隆看看是否真的存在与数据库中,当布隆过滤器说不存在时,那么这条数据一定不存在与数据库中,可以直接告诉客户端不存在这条数据了

但是布隆过滤器说有这条数据,那么这条数据不一定存在数据库中

在这里插入图片描述

@Autowired
private RBloomFilter<String> myBloomFilter;

@SneakyThrows
@Override
public Object query(RequestParam requestParam) {
    String redisKey = String.format("business_key_prefix_%s", requestParam.getId());
    // 从缓存中查询
    Object redisData = stringRedisTemplate.opsForValue().get(redisKey));
    if (redisData != null) {
        return redisData;
    }
    // 从布隆过滤器中判断是否存在-误判(布隆有不一定有, 布隆无就一定无)
    boolean contains = myBloomFilter.contains(redisKey);
    if (!contains) {
        return null;
    }
    // ...
}

4. 判断是否已缓存了无效的 Key

如果缓存和数据库都查不到某个 Key 的数据就写一个到 Redis 中去并设置一个短暂的 ttl,这样做是为了防止有人一直用无效的 Key 恶意攻击,让每次请求都打到数据库中。

在这里插入图片描述

@Autowired
private RBloomFilter<String> myBloomFilter;

@SneakyThrows
@Override
public Object query(RequestParam requestParam) {
    String redisKey = String.format("business_key_prefix_%s", requestParam.getId());
    // 从缓存中查询
    Object redisData = stringRedisTemplate.opsForValue().get(redisKey));
    if (redisData != null) {
        return redisData;
    }
    // 从布隆过滤器中判断是否存在-误判(布隆有不一定有, 布隆无就一定无)
    boolean contains = myBloomFilter.contains(redisKey);
    if (!contains) {
        return null;
    }
    // 判断key是否存在空值-解决缓存穿透(不存在于缓存中,也不存在于数据库中)
    redisData = stringRedisTemplate.opsForValue().get("data_is_null"+redisKey);
    if (redisData != null) {
        return null;
    }
    // ...
}

4. 加锁查数据库,若无数据则缓存空值

在这里插入图片描述

这里使用了 Redisson 中的 getLock.lock() 该方法会一直阻塞,直到锁释放,这样就能做到仅允许一个请求访问数据库

注:当锁释放后其他线程查缓存即可,因为获取到锁的线程已经重建缓存了,如果不这样做,所有等待的线程还是会查数据库,对数据库造成压力。

@Autowired
private RBloomFilter<String> myBloomFilter;

@SneakyThrows
@Override
public Object query(RequestParam requestParam) {
    String redisKey = String.format("business_key_prefix_%s", requestParam.getId());
    // 从缓存中查询
    Object redisData = stringRedisTemplate.opsForValue().get(redisKey));
    if (redisData != null) {
        return redisData;
    }
    // 从布隆过滤器中判断是否存在-误判(布隆有不一定有, 布隆无就一定无)
    boolean contains = myBloomFilter.contains(redisKey);
    if (!contains) {
        return null;
    }
    // 判断key是否存在空值-解决缓存穿透(不存在于缓存中,也不存在于数据库中)
    redisData = stringRedisTemplate.opsForValue().get(redisKey);
    if (redisData != null) {
        return null;
    }
    RLock lock = redissonClient.getLock(String.format("business_lock_key_%s", requestParam.getId()));
    // 上锁-查询数据库-防止被布隆过滤器误判的请求大量打到数据库
    try {
        lock.lock();
        // 再次从缓存中查询
        redisData = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (redisData != null) {
            return redisData;
        }
        // 从数据库中查询
        Object DbData = XxxMapper.selectOne(requestParam);
        // 数据库中没有数据
        if (DbData == null) {
            // 缓存无效的 key,防止缓存穿透
            stringRedisTemplate.opsForValue().set(
                String.format("business_lock_key_%s", requestParam.getId()),
                "-", 5, TimeUnit.MINUTES
            );
            return null;
        }
        // 重建缓存
        stringRedisTemplate.opsForValue().set(
            String.format("business_lock_key_%s", requestParam.getId()),
            DbData,
            // 固定时间+随机时间(防止缓存雪崩)
            10000 + new Random().nextLong(1800 + 1),
            TimeUnit.MICROSECONDS
        );
        return DbData;
    } finally {
        lock.unlock();
    }
}

当然上述这种方案还是会存在问题,若有恶意用户一直使用随机值来请求,这样不仅查了数据库还缓存了无效的 Key,所以还需要配置限流的手段。

可以看看我写过的限流相关的博客:基于Spring AOP与Redisson的令牌桶限流注解实践

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tiantian17)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值