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吧
第一步,导入依赖
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有两种方式,一种是手动集成,一种是自动配置
方式一
导入依赖
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/,这个网站可以看指定数据量和错误率的情况下所需要的空间。
@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,尽快恢复缓存集群。
如果是热点数据过期可参考缓存击穿解决方案。