Redis之缓存穿透问题解决方案实践SpringBoot3+Docker

时间:2024-02-22 17:17:41

文章目录

  • 一、介绍
  • 二、方案介绍
  • 三、Redis Docker部署
  • 四、SpringBoot3 Base代码
      • 1. 依赖配置
      • 2. 基本代码
  • 五、缓存优化代码
      • 1. 校验机制
      • 2. 布隆过滤器
      • 3. 逻辑优化

一、介绍

当一种请求,总是能越过缓存,调用数据库,就是缓存穿透。
在这里插入图片描述
比如当请求一个数据库没有的数据,那么缓存也不会有,然后就一直请求,甚至高并发去请求,对数据库压力会增大。

二、方案介绍

  1. 如果key具有某种规则,那么可以对key增加校验机制,不符合直接返回。
  2. Redisson布隆过滤器
  3. 逻辑修改,当数据库没有此数据,以nullvalue,也插入redis缓存,但设置较短的过期时间。

三、Redis Docker部署

docker-compose示例如下,redis.conf从这里下载

redis:
    container_name: redis
    image: redis:7.2
    volumes:
      - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
    ports:
      - "6379:6379"
    command: [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

四、SpringBoot3 Base代码

1. 依赖配置

		<!-- redis -->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- redis 连接线程池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.11.1</version>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.24.3</version>
        </dependency>
spring: 
  data:
    redis:
      host: 192.168.101.65  # Redis服务器的主机名或IP地址
      port: 6379  # Redis服务器的端口号
      password:  # 用于连接Redis服务器的密码
      database: 0  # 要连接的Redis数据库的索引号
      lettuce:
        pool:
          max-active: 20  # 连接池中最大的活跃连接数
          max-idle: 10  # 连接池中最大的空闲连接数
          min-idle: 0  # 连接池中最小的空闲连接数
      timeout: 10000  # 连接超时时间(毫秒)
      lock-watchdog-timeout: 100  # Redisson的分布式锁的看门狗超时时间(毫秒)

2. 基本代码

要演示的代码很简单,就是一个携带courseId请求过来,调用下面的service函数,然后查询数据库。

@Override
    public CoursePublish getCoursePublish(Long courseId) {
        return coursePublishMapper.selectById(courseId);
    }

当我们使用redis改造时,基本代码如下

@Override
    public CoursePublish getCoursePublishCache(Long courseId) {
        String key = "content:course:publish:" + courseId;
        //先查询redis
        Object object = redisTemplate.opsForValue().get(key);
        if (object != null){
            String string = object.toString();
            CoursePublish coursePublish = JSON.parseObject(string, CoursePublish.class);
            return coursePublish;
        }else {
            //后查询数据库
            CoursePublish coursePublish = getCoursePublish(courseId);
            if (coursePublish != null){
                redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish));
            }
            return coursePublish;
        }
    }

五、缓存优化代码

1. 校验机制

我这里的id没规则,所以加不了,跳过。

2. 布隆过滤器

读取yaml配置

@Data
@Component
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedisProperties {
    private String host;
    private int port;
    private String password;
    private int database;
    private int lockWatchdogTimeout;
}

配置RedissonClient

@Slf4j
@Configuration
public class RedissionConfig {

    @Autowired
    private RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
        RedissonClient redissonClient;

        Config config = new Config();
        //starter依赖进来的redisson要以redis://开头,其他不用
        String url = "redis://"+ redisProperties.getHost() + ":" + redisProperties.getPort();
        config.useSingleServer().setAddress(url)
                //.setPassword(redisProperties.getPassword())
                .setDatabase(redisProperties.getDatabase());

        try {
            redissonClient = Redisson.create(config);
            return redissonClient;
        } catch (Exception e) {
            log.error("RedissonClient init redis url:[{}], Exception:", url, e);
            return null;
        }
    }
}

把布隆过滤器加到service,如下

	private RBloomFilter<String> bloomFilter;

    @PostConstruct
    public void init(){
        //初始化布隆过滤器
        bloomFilter = redissonClient.getBloomFilter("bloom-filter");
        bloomFilter.tryInit(100, 0.003);
        List<CoursePublish> coursePublishList = coursePublishMapper.selectList(new LambdaQueryWrapper<CoursePublish>());
        coursePublishList.forEach(coursePublish -> {
            String key = "content:course:publish:" + coursePublish.getId();
            bloomFilter.add(key);
        });
    }
    
	@Override
    public CoursePublish getCoursePublishCache(Long courseId) {
        String key = "content:course:publish:" + courseId;
        //布隆过滤器
        boolean contains = bloomFilter.contains(key);
        if (!contains){
            return null;
        }
        //先查询redis
        Object object = redisTemplate.opsForValue().get(key);
        if (object != null){
            String string = object.toString();
            CoursePublish coursePublish = JSON.parseObject(string, CoursePublish.class);
            return coursePublish;
        }else {
            //后查询数据库
            CoursePublish coursePublish = getCoursePublish(courseId);
            if (coursePublish != null){
            	bloomFilter.add(key);
                redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish));
            }
            return coursePublish;
        }
    }

3. 逻辑优化

当数据库没有此数据,以nullvalue,也插入redis缓存,但设置较短的过期时间。

            //后查询数据库
            CoursePublish coursePublish = getCoursePublish(courseId);
            if (coursePublish != null) {
            	bloomFilter.add(key);
                redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish));
            }else {
                redisTemplate.opsForValue().set(key, JSON.toJSONString(coursePublish), 10, TimeUnit.SECONDS);
            }
            return coursePublish;

在这里插入图片描述