不了解Redis缓存,拿什么去征服面试官?
她剪了短发 好像温柔了许多 好像过的很好 好像也没那么好 其实我也不知道 我已经很久没有见过她了。
无论是大厂小厂,在项目开发过程中,缓存肯定是离不开的,重要性也毋庸置疑。作为高频面试题,无论是为了应付面试,还是为了学到东西,都必须要认真对待。
Redis缓存
为什么要使用Redis作为缓存,它为什么这么快?
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的。
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
使用多路I/O复用模型(多个网络连接复用一个线程,可以让单个线程高效的处理多个连接请求),非阻塞IO。
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
SpringBoot中整合Redis作为Mybatis的二级缓存
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
开启缓存注解
//配置类添加注解
@EnableCaching
具体操作类使用注解开启缓存使用
//一般作用在查方法上
//在执行方法前先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,执行该方法并将方法返回值放进缓存。
//参数:value缓存名、 key缓存键值、 condition满足缓存条件、unless否决缓存条件
@Cacheable
//一般作用于增加修改方法
//和 @Cacheable 类似,但会把方法的返回值放入缓存中
@CachePut
//一般作用于删除方法上
//方法执行成功后会从缓存中移除相应数据。
//value缓存名、 key缓存键值、 condition满足缓存条件、 unless否决缓存条件、 allEntries是否移除所有数据(设置为true时会移除所有缓存)
@CacheEvict
针对问题解决:
生成key过于简单,容易冲突------自定义KeyGenerator
无法设置过期时间,默认过期时间为永久不过期------自定义cacheManager,设置缓存过期时间
配置序列化方式,默认的是序列化JDKSerialazable------配置类自定义序列化方式
Redis实现分布式集群环境Session共享
在搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理。如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A、B两台服务器,用户在第一次访问网站时,Nginx通过其负载均衡机制将用户请求转发到A服务器,这时A服务器就会给用户创建一个Session。当用户第二次发送请求时,Nginx将其负载均衡到B服务器,而这时候B服务器并不存在Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。
我们应当对产生的Session进行处理,通过粘性Session,Session复制或Session共享等方式保证用户的体验度。
使用Redis开操作共享Session
导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
配置类添加注解开启Session共享
//参数设置缓存时间
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 50)
redis保存的session格式为:
spring:session:sessions:expires:+‘sessionId’
缓存的收益和成本
收益
高速读写------缓存加速读写速度
降低后端负载------降低关系型数据库负载压力
成本
数据不一致------更新策略有问题将导致缓存数据和数据库数据不一致问题
代码维护成本------不仅要维护数据库,还要维护缓存
堆内缓存内存溢出------一般缓存方式分为堆内缓存和远程服务器缓存(redis),堆内缓存性能更高,不需要进行网络传输,远程缓存需要套接字传输。用户级别缓存建议远程缓存,大数据量建议远程缓存,服务节点化原则。
缓存可能会出现的问题
缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU 和内存造成巨大压力,严重的会造成数据库宕机。
◆解决
加锁排队------排斥锁
数据预热------可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key。
双层缓存策略------C1为原始缓存,C2为拷贝缓存,C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期。
定时更新缓存策略------ 失效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存。
设置不同的过期时间------让缓存失效的时间点尽量均匀
详细说一下加锁排队
即第一个线程过来读取cache,发现没有,就去访问DB。后续线程再过来就需要等待第一个线程读取DB成功,cache里的value变得可用,后续线程返回新的value。
用到了加锁排队,吞吐率是不高的。仅适用于并发量不大的场景。
伪代码
public Object getCacheValue(String key, int expiredTime) {
Object cacheValue = cache.get(key);
if (cacheValue != null) {
return cacheValue;
} else {
try {
if (DistributeLock.lock(key)) {
cacheValue = cache.get(key);
if (cacheValue != null) { // double check
return cacheValue;
} else {
cacheValue = GetValueFromDB(); // 读数据库
cache.set(key, cacheValue, expiredTime);
}
}
} finally {
DistributeLock.unlock(key);
}
return cacheValue;
}
}
缓存穿透
缓存和数据库中都没有的数据,而用户不断发起请求,如果攻击者入侵,可能造成数据库压力大。
这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
◆解决
缓存空对象------如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。
布隆过滤器------将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个bitmap 拦截掉,从而避免了对底层存储系统的查询压力(类似Set),为什么不用Set?因为布隆过滤器占用内存空间很小,bit存储。性能特别高。