Redis三大问题解决方案

4/28/2023 缓存框架

# Springboot解决Redis的缓存穿透、缓存击穿、缓存雪崩

# 缓存穿透

缓存穿透指的是一个缓存系统无法缓存某个查询的数据,从而导致这个查询每一次都要访问数据库。 常见的Redis缓存穿透场景包括:

    1. 查询一个不存在的数据:攻击者可能会发送一些无效的查询来触发缓存穿透。
    1. 查询一些非常热门的数据:如果一个数据被访问的非常频繁,那么可能会导致缓存系统无法处理这些请求,从而造成缓存穿透。
    1. 查询一些异常数据:这种情况通常发生在数据服务出现故障或异常时,从而造成缓存系统无法访问相关数据,从而导致缓存穿透。

# 解决方案-布隆过滤器

  • 我们可以使用 Guava 在内存中维护一个布隆过滤器

    1. 添加依赖
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
    1. 创建一个BloomFilterUtil类,用于在缓存中维护Bloom Filter
public class BloomFilterUtil {

    // 布隆过滤器的预计容量
    private static final int expectedInsertions = 1000000;

    // 布隆过滤器误判率
    private static final double fpp = 0.001;

    private static BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp);

    /**
     * 向Bloom Filter中添加元素
     */
    public static void add(String key){
        bloomFilter.put(key);
    }

    /**
     * 判断元素是否存在于Bloom Filter中
     */
    public static boolean mightContain(String key){
        return bloomFilter.mightContain(key);
    }

}
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
    1. Controller中查询数据时,先根据请求参数进行Bloom Filter的过滤
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@GetMapping("/user/{id}")
public User getUserById(@PathVariable Long id){

    // 先从布隆过滤器中判断此id是否存在
    if(!BloomFilterUtil.mightContain(id.toString())){
        return null;
    }

    // 查询缓存数据
    String userKey = "user_"+id.toString();
    User user = (User) redisTemplate.opsForValue().get(userKey);
    if(user == null){

        // 查询数据库
        user = userRepository.findById(id).orElse(null);

        if(user != null){
            // 将查询到的数据加入缓存
            redisTemplate.opsForValue().set(userKey, user, 300, TimeUnit.SECONDS);
        }else{
            // 查询结果为空,将请求记录下来,并在布隆过滤器中添加
            BloomFilterUtil.add(id.toString());
        }
    }
    return user;
}
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

# 缓存击穿

缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。 缓存击穿的原因通常有以下几种:

    1. 缓存中不存在所需的热点数据:当系统中某个热点数据需要被频繁访问时,如果这个热点数据最开始没有被缓存,那么就会导致系统每次请求都需要直接查询数据库,造成数据库负担。
    1. 缓存的热点数据过期:当一个热点数据过期并需要重新缓存时,如果此时有大量请求,那么就会导致所有请求都要直接查询数据库。

# 解决方案-加锁

对于缓存击穿的解决方案就是加锁。当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿。

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronizedLock)就够了。

  • 伪代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@GetMapping("/user/{id}")
public User getUserById(@PathVariable Long id){

    // 先从缓存中获取值
    String userKey = "user_"+id.toString();
    User user = (User) redisTemplate.opsForValue().get(userKey);

    if(user == null){
        // 查询数据库之前加锁
        String lockKey = "lock_user_"+id.toString();
        String lockValue = UUID.randomUUID().toString();

        try{
            Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 60, TimeUnit.SECONDS);

            if(lockResult != null && lockResult){
                // 查询数据库
                user = userRepository.findById(id).orElse(null);
                if(user != null){
                    // 将查询到的数据加入缓存
                    redisTemplate.opsForValue().set(userKey, user, 300, TimeUnit.SECONDS);
                }
            }
        }finally{
            // 释放锁
            if(lockValue.equals(redisTemplate.opsForValue().get(lockKey))){
                redisTemplate.delete(lockKey);
            }
        }
    }
    return user;
}
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

# 缓存雪崩

缓存雪崩指缓存中大量数据的失效时间集中在某一个时间段,导致在这个时间段内缓存失效并额外请求数据库查询数据的请求大量增加,从而对数据库造成极大的压力和负荷。

常见的Redis缓存雪崩场景包括:

    1. 缓存服务器宕机:当缓存服务器宕机或重启时,大量的访问请求将直接命中数据库,并在同一时间段内导致大量的数据库查询请求,从而将数据库压力大幅提高。
    1. 缓存数据同时失效:在某个特定时间点,缓存中大量数据的失效时间集中在一起,这些数据会在同一时间段失效,并且这些数据被高频访问,将导致大量的访问请求去查询数据库。
    1. 缓存中数据过期时间设计不合理:当缓存中的数据有效时间过短,且数据集中在同一时期失效时,就容易导致大量的请求直接查询数据库,加剧数据库压力。
    1. 波动式的访问过程:当数据的访问存在波动式特征时,例如输出某些活动物品或促销商品时,将会带来高频的查询请求访问,导致缓存大量失效并产生缓存雪崩。

# 解决方案

在遇到缓存雪崩时,我们可以使用两种方法:一种是将缓存过期时间分散开,即为不同的数据设置不同的过期时间;另一种是使用Redis的多级缓存架构,通过增加一层代理层来解决。

    1. 添加有关依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.6</version>
</dependency>
1
2
3
4
5
6
7
8
9
    1. properties中配置Ehcache缓存
spring.cache.type=ehcache
1
    1. 创建一个CacheConfig类,用于配置Ehcache
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public EhCacheCacheManager ehCacheCacheManager(CacheManager cm){
        return new EhCacheCacheManager(cm);
    }
    @Bean
    public CacheManager ehCacheManager(){
        EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean();
        cmfb.setConfigLocation(new ClassPathResource("ehcache.xml"));
        cmfb.setShared(true);
        return cmfb.getObject();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    1. ehcache.xml中添加缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
    updateCheck="true"
    monitoring="autodetect"
    dynamicConfig="true">

    <cache name="userCache" maxEntriesLocalHeap="10000" timeToLiveSeconds="60" timeToIdleSeconds="30"/>

</ehcache>
1
2
3
4
5
6
7
8
9
10
    1. 查询数据时,先从Ehcache缓存中获取
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private CacheManager ehCacheManager;

@GetMapping("/user/{id}")
@Cacheable(value = "userCache", key = "#id")
public User getUserById(@PathVariable Long id){
    // 先从Ehcache缓存中获取
    String userKey = "user_"+id.toString();
    User user = (User) ehCacheManager.getCache("userCache").get(userKey).get();
    if(user == null){
        // 再从Redis缓存中获取
        user = (User) redisTemplate.opsForValue().get(userKey);
        if(user != null){
            ehCacheManager.getCache("userCache").put(userKey, user);
        }
    }
    return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21