项目中缓存的使用

导读:本篇文章讲解 项目中缓存的使用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目录

目录

Redis缓存

商品详情页动态内容展示实现的操作:

Redis缓存为啥必须设置缓存失效时间:

本地缓存

项目中本地热点缓存的方案:

Guava Cache简介

本地缓存在项目中的体现:

本地热点缓存的设置 

本地缓存过期时间问题: 

总结:

redis缓存和本地缓存混用的优缺点

面试题:项目中为什么要使用缓存?

面试题:redis的过期策略?内存淘汰机制都有哪些?说一下LRU代码实现?

面试题:如何保证缓存与数据库的双写一致性?

为什么要使用RocketMQ?

面试题: 缓存穿透、 缓存雪崩、 缓存击穿

现在的项目是如何解决缓存穿透、 缓存雪崩、 缓存击穿的? 

面试题:redis 的线程模型是什么?为什么 redis 单线程却能支撑高并发?

redis 的线程模型

为啥 redis 单线程模型也能效率这么高?

面试题:redis 都有哪些数据类型?

面试题:Rdeis线程安全问题



项目中的缓存是多级缓存,取数据的逻辑是:本地缓存 -> redis缓存 -> 数据库

项目中对于缓存的使用仅限于
商品详情页动态内容的展示实现。

Redis缓存

Redis简介

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

Redis 与其他 key – value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

本项目将单机版的Redis作为中间件使用,来完成性能优化的一些操作

Redis缓存有三种模式:1)单机版;2)sentinal哨兵模式;3)集群cluster模式

使用Redis做商品详情页动态获取接口的缓存的内容的实现,是企业级应用第一个常用的优化点;在SpringMVC的Controller层将对应的Redis引入,将从下游Service层(数据库)接收到的一些数据在Controller层缓存起来,以便于下次再有任何的请求进来的时候,可以直接判断缓存中是否有对应的商品详情页的数据,如果有,直接返回而不去走下游的Service层(数据库)调用,减少对数据库的依赖。

最好的优化方式就是在Controller层,在访问对应的商品库存信息之前完成缓存的操作

redis配置

引入依赖

项目中缓存的使用

项目中缓存的使用

商品详情页动态内容展示实现的操作:

如果使用 Redis 默认序列化机制,往Redis中存储对象的结果展示:

项目中缓存的使用

可以看到在界面中,值都被序列化成这么一串字符串了,不便于数据的查看。

将 Redis 的默认序列化机制,改成 JSON 格式的序列化机制最佳,方便商品详情页动态内容展示

Redis的序列化机制:

Redis 的默认序列化机制是JDK序列化机制,下图的源码中可以看出:

项目中缓存的使用

从源码中也可以看出,如果没有设置序列化机制,则defaulatSeralizer = new JdkSerializationRedisSerializer() ,可以明显看出,使用的就是JDK 的序列化机制。

JDK默认序列化机制并非不能使用,只是它具有一定的局限性

  • 它只适用于Java项目,对其他语言编写的项目不兼容,如Go或者PHP

  • 在Redis的可视化页面,无法进行较好的展示

需要将 Redis 的默认序列化机制改为JSON格式,一方面兼容性较高,另一方面方便商品详情页动态内容展示。

项目中缓存的使用

项目中缓存的使用项目中缓存的使用项目中缓存的使用

将商品展示页面的Java对象数据序列化为字节序列的格式以便于保存到Redis数据库中, 并且将日期序列化为自定义的指定格式。Redis中的key是String类型数据,value是Json数据类型。

redis默认的序列化方式就是JDK的序列化方式;这里需要将数据反序列化为Json格式返回给前端

项目中缓存的使用

 项目中缓存的使用项目中缓存的使用

第一次刷新不会出现上面的错误,因为是直接从数据库中取的;第二次刷新错误报这个错误的原因是对下面的字节序列反序列化不了,因为解析不了对应的参数

项目中缓存的使用

加入一行代码:

//可以在序列化的字节序列中包含类信息,以便于反序列化的时候JVM咋样解析参数

objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);     

可以看到此时的字节序列中多了类的信息,加入这行代码的作用就是为了告诉JVM咋样去解析对应的参数;此时,无论刷新多少次都是从缓存(Redis)里取的

项目中缓存的使用

Redis缓存为啥必须设置缓存失效时间:

对应的缓存必须要有失效时间,不仅仅是为Redis的容量性能考虑,更为了对应的业务的能力,如果说缓存没有失效时间,那就必定当数据发生改变的时候,需要有一个清理缓存的机制;首先使用被动的数据过期失效方式,也就是说,不管后台数据库的数据咋么变,Redis默认给一个10分钟的失效时间。

ItemController类中 

项目中缓存的使用

Redis的缺点:

redis缓存的管理的优势是集中式的key-value对的数据库,并且支持数据库 –持久化操作,但是正是由于一致性管理的特性,导致了所有对于数据库的存或者取都需要经过网络的IO达到对应的redisService上(I/0开销是耗时的主要原因:因为redis的数据存在内存上),并且根据redis协议去更新对应的状态,因此相对于操作本地缓存的特性来说,redis的性能此时是并不是最高要求的;因此实现多级缓存第二级,第一级(最底层的是redis),在这之上,引入本地热点缓存。

本地缓存

本地热点缓存的特点:

1)只有热点数据才能进入本地缓存,因此叫做本地热点缓存    
2)脏读非常不敏感
3)内存可控
本地缓存缓存其实就是JVM缓存,(查看一下项目的设置!!!)应用程序启动的时候,将堆栈的大小设置为2048M;这两G的运行空间里面除了要运行Java自身的字节文件以及对应的操作系统所必须要运行的内存之外,还需要做对象的管理;本地缓存其实就是属于对象内存管理区间内的一些堆栈的内存占用信息,因此这些内存其实是非常宝贵的,不可能将数据库中所有的数据加载到本地缓存当中,也不可能将所有的商品详情信息加载到缓存当中,对应缓存的利用效率是要非常高的,因此需要存放热点数据,热点数据每秒钟的访问量是非常大的,这样可以减少服务端到redis中取数据对应的网络开销,也可以减少redisService的压力。脏读非常不敏感加上内存可控就意味着本地热点缓存的生命周期必定不会特别长;对应的本地缓存的生命周期往往要比redis的key的生命周期要短很多,这样才能做到被动的失效对应的脏读的时间控制是非常非常小的。
项目中缓存的使用

 应用程序启动的时候,堆栈的大小设置为2048M

项目中本地热点缓存的方案:

设想一下,做一个类似于Java中HashMap的数据结构,key就是item的id,value就是对应的itemModel的模型即可;做这样的hashmap很有挑战,要支持有并发读有并发写的能力,此时想到ConcrentHashMap,但是它是基于段的方式处理锁的,写锁加上去之后会对写锁的性能产生影响;做到本地缓存就必须考虑对应的失效的淘汰时间以及对应的LRU缓存,所以本项目使用谷歌的Guava cache组件;其本质上也是一个可并发的hashmap,但是它可以控制key的大小和超时时间还可以配置LRU的策略,其次它还是线程安全的

Guava Cache简介

Guava Cache是 Google 提供的一套 Java 工具包,是一套非常完善的本地缓存机制(JVM缓存)。Guava Cache 的设计来源于 CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值,且保持很高的并发读写能力。

Guava Cache的优点:

1)可控制的大小和缓存过期时间

2)可配置的LRU策略(冷数据就不会长期占据内存空间)

3)线程安全

guava介绍的好文章:

https://www.jianshu.com/p/b3c10fcdbf0f

官网(慕课老师推荐)

[Google Guava] 3-缓存 | 并发编程网 – ifeve.com

本地缓存在项目中的体现:

引入对应的jar包

项目中缓存的使用

之前使用redis中间件完成对应的数据库的存取操作,现在封装一个类似于cacheService来完成guava的存和取

项目中缓存的使用

本地热点缓存的设置 

这里cache的使用基本和map的使用一样,但是它提供了初始化的方式可以容许程序员配置最大、最小值以及对应的过期时间、过期策略

项目中缓存的使用

LRU算法 

LRU 算法优势在于算法实现简单,对于对于热点数据, LRU 效率会很好,所以很常用;

缺点是:如果偶然性的要对全量数据进行遍历,那么“历史访问记录”就会被刷走,造成污染;导致缓存命中率下降,减慢了正常数据查询。

缓存命中率是缓存系统的非常重要指标,如果缓存系统的缓存命中率过低,将会导致查询回流到数据库,导致数据库的压力升高。

改进方案:

将链表拆分成两部分,分为热数据区,与冷数据区。

项目中缓存的使用

改进之后算法流程将会变成下面一样:

访问数据如果位于热数据区,与之前 LRU 算法一样,移动到热数据区的头结点。
插入数据时,若缓存已满,淘汰尾结点的数据。然后将数据 插入冷数据区的头结点。
处于冷数据区的数据每次被访问需要做如下判断:
若该数据已在缓存中超过指定时间,比如说 1 s,则移动到热数据区的头结点。
若该数据存在在时间小于指定的时间,则位置保持不变。
对于偶发的批量查询,数据仅仅只会落入冷数据区,然后很快就会被淘汰出去。热门数据区的数据将不会受到影响,这样就解决了 LRU 算法缓存命中率下降的问题。 

LRU策略引入leetcode算法帮助理解学习:

146. LRU 缓存

本地缓存过期时间问题: 

过期时间是一个相对的时间,到底是相对写入时间还是相对于被访问的时间?

一般设置为相对写入时间,因为被访问的时间对于热点数据如果一直被访问的话永远也失效不了,就不合理

总结:

本项目中缓存策略是Redis结合本地缓存,秒杀业务下,高频读取缓存对Redis压力很大,使用本地缓存结合Redis缓存使用,降低Redis压力,同时本地缓存没有连接开销,性能更优。

redis缓存和本地缓存混用的优缺点

优点

  • redis保证数据可持久,本地缓存保证超高的读取性能,秒杀场景下能有效降低redis压力

  • guava作为本地缓存,提供了丰富的api,过期策略,最大容量,保证服务内存可控,冷数据不会长期占据内存空间

  • 服务重启导致的本地缓存清空不会影响业务进行

缺点

  • 只适用于缓存内容只增不改的场景

  • 会产生一定的延时,这个延时具体的影响将会根据业务的差别而定。

面试题:项目中为什么要使用缓存?

缓存的应用场景是读多写少。秒杀场景中,读的流量是远远大于写流量的。
缓存主要是解决读的问题。写还是每次都要走数据库。但是读是走缓存。
所以,缓存解决了读这一部分的问题。

1. 提升性能

1.数据库里包含复杂功能关键字的sql,都很慢。
2.数据库是磁盘,缓存是内存。所以数据库很慢。
3.数据库索引的数据结构是b+树(速度是logN),redis是map(速度是1)。所以数据库很慢。

2. 缓解数据库压力

当用户请求增多时,数据库的压力将大大增加,通过缓存能够大大降低数据库的压力。所有的socket连接都很耗资源,不管是本地socket,还是网络socket。

3. 使用缓存后会产生什么样的问题?

  • 缓存与数据库双写不一致

  • 缓存雪崩、缓存穿透

  • 缓存并发竞争

面试题:redis的过期策略?内存淘汰机制都有哪些?说一下LRU代码实现?

Redis 选择【惰性删除+定期删除】这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

定期删除:每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,所以还要用到惰性删除。
惰性删除:
获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。
Redis结合了定期删除和惰性删除,基本上能很好的处理过期数据的清理,但是实际上还是有点问题的,如果过期key较多,定期删除漏掉了一部分,而且也没有及时去查,即没有走惰性删除,那么就会有大量的过期key堆积在内存中,导致redis内存耗尽,此时又应该咋么办呢?
答案是:走内存淘汰机制。
内存淘汰机制:五六个呢(LFU等),最常用的就是LRU
(注意和本地缓存的过期策略区分,本地缓存的过期策略是LRU)
项目中缓存的使用

面试题:如何保证缓存与数据库的双写一致性?

引入rocketmq,利用rocketmq的事务消息最终解决数据的最终一致性
采用异步消息队列(rocketmq)的方式,将异步扣减的消息同步给消息的consumer端,并由消息的consunmer端完成数据库扣减的操作:
(1)活动发布同步库存进缓存
(2)下单交易减缓存库存
(3)异步消息扣减数据库内存

弱一致性(最终一致性)

最终一致性是能忍受一定时间内的数据不一致性的,只要求最后的数据是一致的即可。缓存一般是设有失效时间的,失效之后数据也会保证一致性,或者是下次修改时,没有并发,也会让数据回到一致性等等;本项目中缓存的失效时间是10分钟。

具体的实现过程为:

  • 如果秒杀商品库存尚有,则生成一条秒杀消息发送到消息队列中(信息中含有用户信息与商品id);
  • 消息的消费者收到秒杀消息后,从数据库中读取用户是否已经完成秒杀,如果没有,则减库存,下订单,写入订单信息到数据库中。

为什么要使用RocketMQ?

答: 为了redis挂的时候不会丢数据
引入RocketMQ:一可以解决redis和数据库一致性的问题,二是减少库存行锁竞争,先执行creatOrder事务,再异步执行减库存,这样可以减少事务持锁时间减少行锁竞争;
异步处理简化秒杀请求中的业务流程,提升系统的性能

引入事务型消息RocketMQ是为了解决redis和数据库最终一致性的问题,但是还是会存在消息回滚,数据库扣减失败,redis和数据库不一致的问题,那么为什么要引入事务性消息呢?

防止redis挂了以后数据库有问题,
redis挂了就系统不可用,因为无法确保数据库的数据和redis是同步的

第一阶段中,redis减库存成功而下单db操作失败了,最终数据库的库存是不会减的,这时候redis和数据库库存不是不一致吗?如果不一致,那么和不使用事务消息的方案不是没有什么区别吗?

redis如果扣减成功了,下单失败会导致redis库存无法回滚,这种情况下业务是可以接受的,除非redis也使用事务型操作,否则没办法和下单请求共享事务;但是用了事务性能会降低,因此这里假定redis扣减成功后下单失败的概率近乎很小,因为所有的验证等操作都提前做完了,除非db挂了。

产品展示中的库存数是用redis中的还是数据库中的?

关于展示问题,按照redis中取,取不出来再取数据库的,若数据库内数据更新,比如下单成功,则发送异步消息去清除redis数据,这样下次过来就可以走数据库拿到正确的数据了,当然也会有扣减库存没有清redis快,但业务上对库存还剩多少件展示层面没必要那么实时。

目前redis和provider消息是符合一致性了,但如果消息consumer处理失败,依旧无法保证redis和数据库最终事务一致?

consumer处理失败分为两种情况:
  1. 程序退出或网络问题等,这种mq不会被标记为消费成功,mq会重试直到成功为止
  2. 无论重试多少次都不能consumer成功的消息,目前情况下业务是先扣redis的,因此不会有这种情况
但是对于本项目而言:数据库和redis之间没有办法保证绝对的强一致,所以宁可少卖不要超卖

面试题: 缓存穿透、 缓存雪崩、 缓存击穿

缓存穿透

当用户访问的数据,
既不在缓存中,也不在数据库中
,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是
缓存穿透
的问题。
缓存穿透的发生一般有这两种情况:
  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
应对缓存穿透的方案,常见的方案有两种。
  • 第一种方案,非法请求的限制;
  • 第二种方案,缓存空值或者默认不存在的值;
1.非法请求的限制
当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
2. 缓存空值或者默认不存在的值
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,可以将空结果(null)或者默认不存在的值存入到缓存中进行防止。
注: 尽量不要使用null做防止击穿的手段,因为如果用null代码判断不出来是真的不存在还是就是存了null,这时候必须要用一个默认不存在的值;这个默认不存在值需要在应用层做好判断。

缓存雪崩

缓存雪崩造成的原因是因为我们在做缓存时为了保证内存利用率以及
为了保证缓存中的数据与数据库中的数据一致性,一般在写入数据时都会给定一个过期时间,而就是因为过期时间的设置有可能导致大量的key在同一时间内全部失效,此时来了大量请求访问这些key,而Redis中却没有这些数据,从而导致所有请求直接落入数据库查询,造成
数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃
可以看到,发生缓存雪崩有两个原因:
  • 大量数据同时过期;
  • Redis 故障宕机;
不同的诱因,应对的策略也会不同。

大量数据同时过期

针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:
  • 均匀设置过期时间;
  • 互斥锁;
  • 设置热点数据永不过期;
1. 均匀设置过期时间
如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
2. 互斥锁
当业务线程在处理用户请求时,
如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,等待锁释放后重新读取缓存。(性能会受到很大的影响,不建议,因为使用缓存就是为了性能 )
3.设置热点数据永不过期:
设置热点数据永不过期,避免热点数据的失效导致大量的相同请求落入DB;
缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为
当系统内存紧张的时候,有些缓存数据会被“淘汰”
 。

Redis 故障宕机

针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:
  • 服务熔断或请求限流机制;
  • 构建 Redis 缓存高可靠集群;
注:也可以从恢复角度出发:
恢复角度:Redis 的 RDB+AOF组合持久化策略,方便redis宕机后及时恢复数据

缓存击穿

缓存击穿和缓存雪崩有点类似,都是由于请求的key过期导致的问题,但是不同点在于失效key的数量,对于雪崩而言指的是大量的key失效导致大量请求落入数据库,而对于击穿而言,指的是某一个热点key突然过期,而这个时候又突然又大量的请求来查询此热点key,但是在Redis中却并没有查询到结果从而导致所有请求全部进入数据库,导致在这个时刻数据库直接被打穿。
应对缓存击穿可以采取前面说到两种方案:
  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,等待锁释放后重新读取缓存。
  • 设置热点数据永不过期,避免热点数据的失效导致大量的相同请求落入DB;
    缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为
    当系统内存紧张的时候,有些缓存数据会被“淘汰”
     。

现在的项目是如何解决Redis缓存穿透、 缓存雪崩、 缓存击穿的? 

本地缓存 + 限流 + 合理的缓存失效时间,足以抗住整个秒杀环节;对于redis集群高可用、持久化策略还没有在项目中实现。

Redis中所有对于数据库的存或者取都需要经过网络的IO达到对应的redisService上(I/0开销是耗时的主要原因:因为redis的数据存在内存上),所以使用本地热点缓存;本地热点缓存只有热点数据才可以进入,热点数据每秒钟的访问量是非常大的,这样可以减少服务端到redis中取数据对应的网络开销,也可以减少redisService的压力。

设置合理的失效时间避免Redis缓存和本地缓存同时回源。

面试题:Rdeis线程安全问题

关于取数据判断是否为null的逻辑上面,如果在高并发场景下,不加锁,是否会有线程安全问题,是否该加个双重检查锁?

答: 无需加锁,因为用缓存本身就没有必要保证一定不能脏读,加了锁反而影响性能,就体现不出来做缓存的任何意义

《【面试突击】— Redis篇》–Redis的线程模型了解吗?为啥单线程效率还这么高?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/110892.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!