自学内容网 自学内容网

如何解决Redis缓存穿透

如何解决Redis缓存穿透


本篇将带大家了解如何在不同的业务场景下防范Redis缓存穿透,以查询商品业务场景为例子,分别使用缓存null和布隆过滤器的方法来防范缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样请求到达缓存永远不会命中,并且查询数据库的结果也为null,不会写入缓存,从而使得这些请求都会打到数据库上,在并发量非常高的场景下,大量的请求打到数据库上很有可能会压垮数据库

常规情况下的缓存机制

通常客户端对服务端发出热点数据的查询请求时,会先请求到缓存上,若缓存命中则直接返回数据,若未命中则回去查询数据库,如果在数据库中查到了数据则会返回客户端,并且将数据写入缓存,以便下次查询时,请求可以直接从缓存中获取数据,从而减轻数据库的压力
缓存机制
那么此时就会产生缓存穿透的问题,以一个根据ID查询商品的业务为例:

/**
     * 根据id查询商品
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        // 1.查询redis缓存
        String jsonItem = redisTemplate.opsForValue().get("item:" + id);// key

        // 2.命中直接返回
        if (StrUtil.isNotBlank(jsonItem)) {
            Item item = JSONUtil.toBean(jsonItem, Item.class);
            return Result.ok(item);
        }

        // 3.未命中查询数据库
        Item item = getById(id);

        // 4.数据库中没有返回错误
        if (item == null) {
            return Result.fail("商品不存在!");
        }

        // 5.写入redis
        redisTemplate.opsForValue()  // 设置过期时间30min
        .set("item:" + id, JSONUtil.toJsonStr(item), 30, TimeUnit.MINUTES);

        // 6.返回数据
        return Result.ok(item);
    }

这个案例严格执行了上面的缓存机制,执行步骤为:

  1. 查询redis缓存
  2. 命中则直接返回数据
  3. 未命中则查询数据库
  4. 数据库中无数据则返回错误
  5. 写入redis缓存
  6. 返回数据

此时若是客户端发来一个完全不存在的id的查询请求时,业务逻辑会在第4步时直接返回错误信息,如果有人恶意大量编造虚假ID来对数据库发动大规模的请求攻击时,我们的服务是我无法处理这种情况的,所以为了数据库的安全性考虑,我们必须要解决缓存穿透问题

常规的解决方案有两种:

  • 缓存空对象:当查询到一个不存在的数据时,我们也将空值写入缓存,使得下次请求可以命中缓存

    • 优点:简单粗暴
    • 缺点:容易造成数据短暂不一致、内存消耗增大
  • 布隆过滤:在请求到达缓存之前先利用布隆过滤算法判断对象是否存在,若不存在则会直接拒绝请求

    • 优点:内存消耗较少
    • 缺点:实现复杂,存在误判

缓存空对象

当缓存未命中并且查询数据库后无数据,此时也将空值写入缓存中,在下次查询时,可以直接命中缓存并返回,但是在命中缓存后,要判断是否命中的是空值,如果是控制则返回错误信息,防止将缓存的空对象当作正常的业务对象
内存消耗问题:如果伪造ID发起大规模请求攻击,那么攻击过后缓存中会有许多垃圾数据,也就是我们假如的空对象数据,大大占用了缓存内存,所以我们可以为空对象的缓存添加一个较短的过期时间TTL

/**
     * 根据id查询商品
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        // 1.查询redis
        String jsonItem = redisTemplate.opsForValue().get("item:" + id);

        // 2.命中直接返回
        if (StrUtil.isNotBlank(jsonItem)) {
            Item item = JSONUtil.toBean(jsonItem, Item.class);
            return Result.ok(item);
        }
        // 判断是否命中为空值
        if (jsonItem != null){
            // 返回错误
            return Result.fail("商品不存在");
        }

        // 3.未命中查询数据库
        Item item = getById(id);

        // 4.数据库中没有返回错误
        if (item == null) {
            // 将空值写入缓存
            redisTemplate.opsForValue().set("item:" + id, "", 2, TimeUnit.MINUTES); // 两分钟过期时间
            // 返回错误信息
            return Result.fail("商品不存在");
        }

        // 5.写入redis
        redisTemplate.opsForValue().set("item:" + id, JSONUtil.toJsonStr(item), 30, TimeUnit.MINUTES);

        // 6.返回数据
        return Result.ok(item);
    }

布隆过滤

这是一种空间效率极高的概率型数据结构。它可以用来判断一个元素是否在一个集合中。在缓存系统中,可以将数据库中所有可能存在的数据的键(如商品 ID)放入布隆过滤器中。当一个请求到来时,先通过布隆过滤器进行检查,如果布隆过滤器判断该数据不存在,那么就直接返回,不会再去数据库查询;如果布隆过滤器判断可能存在,再去缓存和数据库中查询。布隆过滤器存在一定的误判率,但可以通过调整参数来降低误判率
布隆过滤
那么布隆过滤器又是怎么知道某个值是否存在呢?
底层原理:底层有一个二进制数组,初始时所有位都被设置为 0,有多个哈希函数,其作用是将输入的元素映射到数组中的位置,一个元素进行多次哈希后,将数据不同位置的值设置为1,此时请求一个元素时,就可以使用该元素多次哈希的值在数组中查询是否所有位置全部为1,若不全部为1则证明该元素一定不存在,从而拒绝请求,但是如果所有位置都为1也不一定证明其一定存在,因为可能存在哈希碰撞的情况
原理
实现

1.引入依赖

在pom.xml文件中添加 Guava 的依赖,用于实现布隆过滤器:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version> 
</dependency>

2.创建布隆过滤器实例

可以创建一个工具类或者在合适的配置类中初始化布隆过滤器,示例如下:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.nio.charset.StandardCharsets;

@Configuration
public class BloomFilterConfig {

    private static final int EXPECTED_INSERTIONS = 10000; // 预期插入的元素数量,预估下数据库中商品ID的数量规模
    private static final double FPP = 0.01; // 误判率,可根据需求调整

    @Bean
    public BloomFilter<Long> ItemIdBloomFilter() {
        return BloomFilter.create(Funnels.longFunnel(StandardCharsets.UTF_8), EXPECTED_INSERTIONS, FPP);
    }
}

3.初始化布隆过滤器数据

需要在合适的时机(比如项目启动后,从数据库加载完初始数据时等)把数据库中已有的店铺 ID 添加到布隆过滤器中,示例代码可以放在一个启动后执行的方法中(比如实现ApplicationRunner接口等方式)

import com.google.common.hash.BloomFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class BloomFilterInitializer implements ApplicationRunner {

    @Autowired
    private ItemService itemService; // 假设你的业务层接口是这个名字,根据实际调整

    @Autowired
    private BloomFilter<Long> ItemIdBloomFilter;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<Long> allItemIds = itemService.getAllItemIds(); // 需在ItemService中定义获取所有商品ID的方法
        for (Long id : allItemIds) {
            ItemIdBloomFilter.put(id);
        }
    }
}

4.在查询方法中使用布隆过滤器进行判断

修改原来的queryById方法,在查询 Redis 之前先通过布隆过滤器判断商品ID 是否可能存在:


    @Autowired
    private BloomFilter<Long> itemIdBloomFilter;

    /**
     * 根据id查询商品
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        // 先通过布隆过滤器判断
        if (!itemIdBloomFilter.mightContain(id)) {
            return Result.fail("商品不存在");
        }

        // 1.查询redis
        String jsonItem = redisTemplate.opsForValue().get("item:" + id);

        // 2.命中直接返回
        if (StrUtil.isNotBlank(jsonItem)) {
            Item item = JSONUtil.toBean(jsonItem, Item.class);
            return Result.ok(item);
        }

        // 3.未命中查询数据库
        Item item = getById(id);

        // 4.数据库中没有返回错误
        if (item == null) {
            return Result.fail("商品不存在");
        }

        // 5.写入redis
        redisTemplate.opsForValue().set("item:" + id, JSONUtil.toJsonStr(item), 30, TimeUnit.MINUTES);
        // 6.返回数据
        return Result.ok(item);
    }

以上两种方法都是被动防范缓存穿透的方法,除此之外还有一些方法可以主动防止缓存穿透:

  • 增强ID的复杂度,增大伪造ID的困难程度
  • 做好数据格式校验,校验ID是否遵循特定规则
  • 做好热点数据限流
  • 加强用户权限校验

原文地址:https://blog.csdn.net/Gaomengsuanjia_/article/details/144297918

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!