vlambda博客
学习文章列表

不了解Redis缓存,拿什么去征服面试官?

她剪了短发 好像温柔了许多 好像过的很好 好像也没那么好 其实我也不知道 我已经很久没有见过她了。

无论是大厂小厂,在项目开发过程中,缓存肯定是离不开的,重要性也毋庸置疑。作为高频面试题,无论是为了应付面试,还是为了学到东西,都必须要认真对待。

Redis缓存

为什么要使用Redis作为缓存,它为什么这么快?

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。

  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的。

  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

  4. 使用多路I/O复用模型(多个网络连接复用一个线程,可以让单个线程高效的处理多个连接请求),非阻塞IO。

  5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

SpringBoot中整合Redis作为Mybatis的二级缓存

  • 引入依赖

 
   
   
 
  1. <dependency>

  2. <groupId>org.springframework.boot</groupId>

  3. <artifactId>spring-boot-starter-cache</artifactId>

  4. </dependency>

  • 开启缓存注解

 
   
   
 
  1. //配置类添加注解

  2. @EnableCaching

  • 具体操作类使用注解开启缓存使用

 
   
   
 
  1. //一般作用在查方法上

  2. //在执行方法前先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,执行该方法并将方法返回值放进缓存。

  3. //参数:value缓存名、 key缓存键值、 condition满足缓存条件、unless否决缓存条件

  4. @Cacheable

 
   
   
 
  1. //一般作用于增加修改方法

  2. //和 @Cacheable 类似,但会把方法的返回值放入缓存中

  3. @CachePut

 
   
   
 
  1. //一般作用于删除方法上

  2. //方法执行成功后会从缓存中移除相应数据。

  3. //value缓存名、 key缓存键值、 condition满足缓存条件、 unless否决缓存条件、 allEntries是否移除所有数据(设置为true时会移除所有缓存)

  4. @CacheEvict

针对问题解决:

  1. 生成key过于简单,容易冲突------自定义KeyGenerator

  2. 无法设置过期时间,默认过期时间为永久不过期------自定义cacheManager,设置缓存过期时间

  3. 配置序列化方式,默认的是序列化JDKSerialazable------配置类自定义序列化方式

Redis实现分布式集群环境Session共享

在搭建完集群环境后,不得不考虑的一个问题就是用户访问产生的session如何处理。如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在A、B两台服务器,用户在第一次访问网站时,Nginx通过其负载均衡机制将用户请求转发到A服务器,这时A服务器就会给用户创建一个Session。当用户第二次发送请求时,Nginx将其负载均衡到B服务器,而这时候B服务器并不存在Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。

我们应当对产生的Session进行处理,通过粘性Session,Session复制或Session共享等方式保证用户的体验度。

使用Redis开操作共享Session

  • 导入依赖

 
   
   
 
  1. <dependency>

  2. <groupId>org.springframework.session</groupId>

  3. <artifactId>spring-session-data-redis</artifactId>

  4. </dependency>

  • 配置类添加注解开启Session共享

 
   
   
 
  1. //参数设置缓存时间

  2. @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 50)

redis保存的session格式为:

 
   
   
 
  1. spring:session:sessions:expires:+‘sessionId

缓存的收益和成本

收益

  1. 高速读写------缓存加速读写速度

  2. 降低后端负载------降低关系型数据库负载压力

成本

  1. 数据不一致------更新策略有问题将导致缓存数据和数据库数据不一致问题

  2. 代码维护成本------不仅要维护数据库,还要维护缓存

  3. 堆内缓存内存溢出------一般缓存方式分为堆内缓存和远程服务器缓存(redis),堆内缓存性能更高,不需要进行网络传输,远程缓存需要套接字传输。用户级别缓存建议远程缓存,大数据量建议远程缓存,服务节点化原则。

缓存可能会出现的问题

缓存雪崩

如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。

由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU 和内存造成巨大压力,严重的会造成数据库宕机。

◆解决

  1. 加锁排队------排斥锁

  2. 数据预热------可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key。

  3. 双层缓存策略------C1为原始缓存,C2为拷贝缓存,C1失效时,可以访问C2,C1缓存失效时间设置为短期,C2设置为长期。

  4. 定时更新缓存策略------ 失效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存。

  5. 设置不同的过期时间------让缓存失效的时间点尽量均匀

详细说一下加锁排队

即第一个线程过来读取cache,发现没有,就去访问DB。后续线程再过来就需要等待第一个线程读取DB成功,cache里的value变得可用,后续线程返回新的value。

用到了加锁排队,吞吐率是不高的。仅适用于并发量不大的场景。

伪代码

 
   
   
 
  1. public Object getCacheValue(String key, int expiredTime) {

  2. Object cacheValue = cache.get(key);

  3. if (cacheValue != null) {

  4. return cacheValue;

  5. } else {

  6. try {

  7. if (DistributeLock.lock(key)) {

  8. cacheValue = cache.get(key);

  9. if (cacheValue != null) { // double check

  10. return cacheValue;

  11. } else {

  12. cacheValue = GetValueFromDB(); // 读数据库

  13. cache.set(key, cacheValue, expiredTime);

  14. }

  15. }

  16. } finally {

  17. DistributeLock.unlock(key);

  18. }

  19. return cacheValue;

  20. }

  21. }

缓存穿透

缓存和数据库中都没有的数据,而用户不断发起请求,如果攻击者入侵,可能造成数据库压力大。

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

◆解决

  1. 缓存空对象------如果一个查询返回的数据为空,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库。

  2. 布隆过滤器------将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个bitmap 拦截掉,从而避免了对底层存储系统的查询压力(类似Set),为什么不用Set?因为布隆过滤器占用内存空间很小,bit存储。性能特别高。