Redis缓存
什么是缓存
以空间换时间,将数据保存到内存中,读写操作更快,减小数据库压力,提高效能
哪些数据适合缓存
-
经常查询的人热点数据
-
不经常变的数据(数据变化会导致缓存中的数据跟着变,如果变化频繁,性能开销很大)
缓存的流程
- 请求查询时,先去缓存中查询,如果有直接返回
- 如果缓存中没有,到数据库查询
- 将数据库查询的数据同步到缓存中
- 返回查询数据
传统缓存方案和分布式缓存方案的区别
- 传统缓存方案将我们的数据保存到本地
- 当我们集群的时候,假如A,B微服务中的缓存数据都是100,如果此时A服务修改了数据库中的数据并且更新了缓存,B却没有修改,此时就出现了缓存不同步的问题
- 使用分布式缓存方法,使用redis作为共享缓存,就解决了缓存不同步的问题,还不占应用内存
- 同时微服务各自也可以将私有的数据保存在本地缓存中,读写更快
微服务集成redis
直接从微服务中调用redis服务器
基础搭建
导入redis缓存依赖
<!--整合Redis , 底层可以用jedis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置redis配置
package cn.hxyjyz.hrm.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;//缓存的配置
import javax.annotation.Resource;
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Resource
private RedisConnectionFactory factory;
/**
* 自定义生成redis-key , 类名.方法名
* @return
*/
@Override
@Bean
public KeyGenerator keyGenerator() {
return (o, method, objects) -> {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName()).append(".");
sb.append(method.getName()).append(".");
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
};
}
//使用JSON进行序列化
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//JSON格式序列化
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//key的序列化
redisTemplate.setKeySerializer(genericJackson2JsonRedisSerializer);
//value的序列化
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
//hash结构key的虚拟化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//hash结构value的虚拟化
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
return redisTemplate;
}
@Bean
@Override
public CacheResolver cacheResolver() {
return new SimpleCacheResolver(cacheManager());
}
@Bean
@Override
public CacheErrorHandler errorHandler() {
// 用于捕获从Cache中进行CRUD时的异常的回调处理器。
return new SimpleCacheErrorHandler();
}
//缓存管理器
@Bean
@Override
public CacheManager cacheManager() {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues() //不允许空值
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));//值使用JSON虚拟化
return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
}
}
手动缓存
- 从redis缓存中获取数据
- 如果有就返回数据
- 如果没有就查询数据库,然后放入缓存中,最后返回数据
@Override
public List<CourseType> list() {
List<CourseType> courseTypes = (List<CourseType>)redisTemplate.opsForValue()
.get(RedisCode.KEY_REDIS_CODE);
if (courseTypes == null) {
//表明缓存中没有,就从数据库中查询
List<CourseType> courseTypeList = getCourseTypes2treeData();
//将数据库中查询出来的放入缓存中
redisTemplate.opsForValue().set(RedisCode.KEY_REDIS_CODE, courseTypeList);
//返回查询的课程树
return courseTypeList;
} else {
//返回缓存中的查询结果
return courseTypes;
}
}
删除缓存
- 复写方法
- 注意删除缓存是先修改数据库,再删除缓存
- 当我们使用注解时,对应的方法必须有接口(才能使用动态代理的方式)
注解缓存:集成SpringCache
-
基础搭建
-
启动类添加注解@EnableCaching开启缓存
-
通过在方法上添加注解@Cacheable进行缓存或查询缓存
当发送请求时,首先通过注解在缓存中查询,如果缓存中没有,到数据库查询,并且将返回值放入缓存中
- 里面有参数cachenames,缓存的名字,以及key,当我们保存到缓存中时,就是cachename::key拼接作为key值保存在redis中
- condition,里面跟我们获取数据的条件
-
常用注解
² @Cacheable:触发缓存写入。
² @CacheEvict:触发缓存清除。
² @CachePut:更新缓存(不会影响到方法的运行)。
² @Caching:重新组合要应用于方法的多个缓存操作。
² @CacheConfig:设置类级别上共享的一些常见缓存设置。
常见问题
你们如何保证mysql和redis数据一致性的
首先,我们将数据库的不一致情况分为三种情况:
数据库有数据,缓存没有数据;
数据有数据,缓存也有数据,数据不相等;
数据库没有数据,缓存有数据;
我们一般使用Cache Aside Pattern的缓存策略来解决不一致问题,简而言之:
1.首先从缓存中读取数据,如果未命中,就从数据库中读取,同步到缓存中并返回
2.需要更新数据库,先更新数据库,在将缓存中的数据清除
解决逻辑:
对于第一种,在读数据的时候会自动将数据库数据同步到缓存中
对于第二种情况,数据变成了不相等,但是在之前某一个时刻,他们一定是相等的,这种不一致,一定是因为更新数据库导致的,而我们的更新策略是先更新数据库,再删除缓存,所以不一致的原因就是删除缓存失败了
对于第三种情况,和第二种情况相似,删除缓存的时候失败了
解决方案:
1.重试删除缓存,要求一致性越高,重试越频繁
2.定期全量更新,比如在每天凌晨一点将所有缓存清空,然后全部重新加载
3.给所有的缓存一个失效期(这个方法非常有效,失效期越短,数据一致性就越高,但是查询数据库加载缓存的频率也就越高,所以根据业务需要来定)
先删除缓存再改数据库和先改数据库后删缓存有什么区别
1.当我们先删除缓存时,此时如果请求来了,就去库读取旧数据,并保存到缓存中
2.然后在修改数据库,此时缓存中就存在的是脏数据
3.以后拿取的就都是脏数据
解决方法:修改完数据库之后,我们再删一次缓存,下次请求来时就会去数据库中拿去数据并保存到缓存中
这里又面临一个问题 (先删数据库后删缓存)
1.当我们修改数据库成功时,还没有删除缓存
2.请求来了,从缓存中拿到脏数据
3.我们删除缓存,下一次请求来的时候,就会去读取数据库更新缓存
所以我们直接选择第二种
说一下SpringCache的常用注解和作用
@Cacheable 进行缓存
@CacheEvict 清除缓存
@CachePut 更新缓存
@Caching 组合多个缓存操作
@CacheConfig 配置一些共有的缓存配置
你们项目中缓存穿透,缓存击穿,缓存雪崩解决方案
缓存穿透
缓存穿透是指当我们请求去查询一个不存在的数据时,缓存中没有,就去数据库中查找,也没有也就无法同步到缓存,下次相同的请求依旧去查询数据库,缓存失去了意义,当并发高时,数据库可能就会挂掉
解决方案
一.将数据库中的空值也放入缓存中,当查询时返回查询的空值,降低访问数据(但是存在一个问题,如果缓存的空值太多,占用更多的空间,我们可以通过设置一个较短的过期时间)
二.将数据库中的数据放入布隆过滤器中,当请求过来时,我们先经过布隆过滤器进行查询,如果存在继续查返回数据,不存在直接丢弃
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案
这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
1.使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
2. "提前"使用互斥锁(mutex key):
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
3. "永远不过期": 最为实用
这里的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
4. 资源保护:
采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可