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

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

一、介绍

当一种请求,总是能越过缓存,调用数据库,就是缓存穿透。

比如当请求一个数据库没有的数据,那么缓存也不会有,然后就一直请求,甚至高并发去请求,对数据库压力会增大。

二、方案介绍

  1. 如果key具有某种规则,那么可以对key增加校验机制,不符合直接返回。
  2. Redisson布隆过滤器
  3. 逻辑修改,当数据库没有此数据,以nullvalue,也插入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. 逻辑优化

当数据库没有此数据,以nullvalue,也插入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;

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

https://xiamu-ssr.github.io/Hexo/2024/02/21/2024-H1/2024-02-21-17-46-46/

作者

Xiamu

发布于

2024-02-21

更新于

2024-08-11

许可协议

评论