SpringBoot进阶-缓存击穿,缓存穿透,缓存雪崩(八)

redis很重要的一个应用就是缓存,缓存是提升系统性能的有效手段,今天我们就来聊聊缓存那些事。

缓存实现

来看一个缓存实现的代码,比较经典了,没啥难度

@GetMapping("getById")
@ApiOperation(value = "根据id获取用户")
public Result getById(int id) {
    String key = USER_KEY + id;
    //先从缓存中取
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user == null) {
        //user不存在则从数据库取出并放入缓存,并且设置过期时间
        user = userService.getById(id);
        redisTemplate.opsForValue().set(key, user, Duration.ofHours(6));
    }
    return resultOk(user);
}

@ApiOperation(value = "修改用户")
@PutMapping
public Result update(@RequestBody User user) {
    //修改和删除时需要同时删除缓存,防止缓存中存在脏数据
    String key = USER_KEY + user.getId();
    redisTemplate.delete(key);
    userService.updateById(user);
    return resultOk();
}

Spring Cache

上面的代码还是有点啰嗦的,有没有偷懒的办法呢?那就用Spring Cache吧

第一步,导入依赖


    org.springframework.boot
    spring-boot-starter-cache

第二步,开启cahe并自定义存储格式

@Configuration
@EnableCaching // 开启缓存支持
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 配置序列化(解决乱码的问题),过期时间30分钟
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }

}

第三步,使用缓存

@GetMapping("getByIdSpringCache")
@ApiOperation(value = "根据id获取用户SpringCache")
@Cacheable(value = "USER", key = "'USER_INFO:' +#p0")
public Result getByIdSpringCache(int id) {
    User user = userService.getById(id);
    return resultOk(user);
}

@ApiOperation(value = "修改用户SpringCache")
@PutMapping("updateSpringCache")
@CacheEvict(value = "USER", key = "'USER_INFO:' +#p0.id")
public Result updateSpringCache(@RequestBody User user) {
    userService.updateById(user);
    return resultOk();
}

可以看到代码比之前简洁多了,一个注解就搞定了。其中#p0是springEL表达式,代表第一个参数值。@Cacheable表示放入缓存,@CacheEvict表示删除缓存

如果并发量很高,就要考虑缓存的三大坑了,缓存击穿,缓存穿透,缓存雪崩。

缓存击穿

缓存击穿是原来有缓存,由于缓存失效,造成缓存被击穿了,然后大量请求涌入到数据库,造成系统卡死。

解决方法是热点key不设置失效时间,或者加锁。下面看加锁方案

private final static String GET_BY_ID_LOCK = "GET_BY_ID_LOCK";

@GetMapping("getById")
@ApiOperation(value = "根据id获取用户")
public Result getById(int id) {
    String key = USER_KEY + id;
    //先从缓存中取
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user == null) {
        //此时有1W个请求到这,不加锁的话1W个请求会直接打到数据库
        synchronized (GET_BY_ID_LOCK) {
            //高并发下需要双重检查,如果还是空说明真的没有缓存
            user = (User) redisTemplate.opsForValue().get(key);
            if (user == null) {
                //user不存在则从数据库取出并放入缓存,并且设置过期时间
                user = userService.getById(id);
                redisTemplate.opsForValue().set(key, user, Duration.ofHours(6));
            }
        }
    }
    return resultOk(user);
}

缓存穿透

缓存穿透指缓存和数据库都没有数据,请求直接穿透了。解决方案是空对象缓存或者布隆过滤器。

空对象缓存的意思是比如查id是1的对象返回null,那就直接把null也放入缓存,但判断缓存中存不存在就要用到exists了,比较麻烦。

还有一种布隆过滤器,以前面试要是能答出这个来瞬间逼格拉满,现在似乎是很平常的八股文了(真是越来越卷了o(╥﹏╥)o)。

布隆过滤器有点像hashmap,由一个大数组和多个hash函数构成,它会把hash值取模后的位置置为1,由于hash冲突,如果他判断元素不存在则一定不存在,如果他判断元素存在则元素有可能存在,会有一定的误判。

不了解也没事,只要记住一句话,布隆过滤器就是用来快速判断某个数据是否存在的。

SpringBoot集成Redisson

SpringBoot集成Redisson有两种方式,一种是手动集成,一种是自动配置

方式一

导入依赖


    org.redisson
    redisson
    3.5.0

 创建redissonClient的bean

@Configuration
public class RedissonManager {
    @Value("${redisson.address}")
    private String addressUrl;

    @Bean
    public RedissonClient getRedisson() throws Exception{
        RedissonClient redisson = null;
        Config config = new Config();
        config.useSingleServer()
                .setAddress(addressUrl);
        redisson = Redisson.create(config);
        return redisson;
    }
}

方式二

导入依赖,可以看到redisson的自动配置底层使用的是spring-data-redis,所以版本要与spring-data-redis的版本一致,比如我的springBoot版本是2.3.X就要用spring-data-23,更详细的可以看官方文档:https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter#spring-boot-starter


    org.redisson
    redisson-spring-boot-starter
    3.16.1
    
        
            org.redisson
            redisson-spring-data-25
        
    


    org.redisson
    
    redisson-spring-data-23
    3.16.1

 配置就可以直接使用spring-data-redis的配置了,

spring:
  redis:
    database: 
    host:
    port:
    password:
    ssl: 
    timeout:
    cluster:
      nodes:
    sentinel:
      master:
      nodes:

 使用布隆过滤器解决缓存穿透

第一步,需要创建一个过滤器,预估数据量和可接受的错误率,然后把所有id放进去,后面新增数据的时候也需要放入id,https://krisives.github.io/bloom-calculator/,这个网站可以看指定数据量和错误率的情况下所需要的空间。

SpringBoot进阶-缓存击穿,缓存穿透,缓存雪崩(八)_第1张图片

@GetMapping("init")
@ApiOperation(value = "初始化")
public Result init() {
    RBloomFilter bloomFilter = redissonClient.getBloomFilter("userIdFilter");
    //预估数据量和可接受的错误率
    bloomFilter.tryInit(1000000, 0.001);
    List users = userService.list();
    for (User user : users) {
        //放入布隆过滤器
        bloomFilter.add(user.getId());
    }
    return resultOk();
}

接下去请求的时候判断id存不存在,不存在直接返回,解决了缓存穿透问题

@GetMapping("getById")
@ApiOperation(value = "根据id获取用户")
public Result getById(int id) {
    RBloomFilter bloomFilter = redissonClient.getBloomFilter("userIdFilter");
    //判断该id是否存在
    if (!bloomFilter.contains(id)) {
        //布隆过滤器特点,判断结果为不存在的时候则一定不存在,可放心返回
        return resultOk();
    }
    //存在的情况布隆过滤器有一定误判可能,
    // 但没关系,错误率为0.001的情况下1W个请求只会有10个打到数据库,可以接受
    String key = USER_KEY + id;
    //先从缓存中取
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user == null) {
        //user不存在则从数据库取出并放入缓存,并且设置过期时间
        user = userService.getById(id);
        redisTemplate.opsForValue().set(key, user, Duration.ofHours(6));
    }
    return resultOk(user);
}

缓存雪崩

redis挂了或者大量热点数据同时过期,导致大量请求 打到数据库引发系统奔溃。

解决方法是1. redis高可用-Redis Cluster,2. 使用sentinel进行限流和降级,3. 开启redis持久化机制aof/rdb,尽快恢复缓存集群。

如果是热点数据过期可参考缓存击穿解决方案。

你可能感兴趣的:(SpringBoot,Redis,缓存,redis)