一、介绍 当一种请求,总是能越过缓存,调用数据库,就是缓存穿透。
比如当请求一个数据库没有的数据,那么缓存也不会有,然后就一直请求,甚至高并发去请求,对数据库压力会增大。
二、方案介绍
如果key
具有某种规则,那么可以对key增加校验机制,不符合直接返回。
Redisson
布隆过滤器
逻辑修改,当数据库没有此数据,以null
为value
,也插入redis
缓存,但设置较短的过期时间。
三、Redis Docker部署 docker-compose示例如下,redis.conf从这里下载
language-yml 1 2 3 4 5 6 7 8 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. 依赖配置 language-xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!-- 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>
language-yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
函数,然后查询数据库。
language-java 1 2 3 4 5 @Override public CoursePublish getCoursePublish(Long courseId) { return coursePublishMapper.selectById(courseId); }
当我们使用redis改造时,基本代码如下
language-java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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
配置
language-java 1 2 3 4 5 6 7 8 9 10 11 12 @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
language-java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @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
,如下
language-java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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. 逻辑优化 当数据库没有此数据,以null
为value
,也插入redis
缓存,但设置较短的过期时间。
language-java 1 2 3 4 5 6 7 8 9 10 11 //后查询数据库 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;