Bootstrap

Redis实战二-商户缓存

目录

一、缓存

二、添加商铺缓存

1.商铺缓存

 2.店铺类型缓存

​三、缓存更新策略

1.原理 

2.实践 

四、缓存穿透

1.原理 

2.实践

五、缓存雪崩

六、缓存击穿

1.原理

2.实践—互斥锁

3.时间—逻辑过期

六、缓存工具封装

​ 七、缓存总结


一、缓存

缓存是数据交换的缓冲区,是存储数据的临时地方,一般读写性能高。

CPU缓存。CPU运算速度快,但读写速度(从内存或磁盘中读数据放到寄存器中)远小于CPU的运算速度,这导致计算机性能受到限制。因此,在CPU中加一个缓存。

web应用开发过程中也需要缓存。

浏览器缓存。用户通过浏览器发送请求,浏览器将页面静态资源缓存在本地,如此便无需每次访问都要去加载这些数据,可以大大地降低网络的延迟,提高页面的响应速度。对于浏览器缓存中未命中的数据,向tomcat发送请求获得。

应用层缓存。把从数据库中查到的数据存到redis中,减少了数据库的查询效率。应用层缓存未命中的,向数据库请求。

数据库缓存。mysql数据库会给id创建索引,将这些索引数据缓存起来,便可以根据这些索引查询,在内存里快速检索得到结果,不用去读写磁盘。排序、表关联等还是需要cpu运算,最终还是会去访问CPU和磁盘。

缓存有利也有弊。 

二、添加商铺缓存

1.商铺缓存

这个接口根据id查询商铺信息,并返回给浏览器。 

 后端直接查询数据库获取信息。

 

我们来将这个业务加入缓存,提高性能。

根据左图的缓存作用模型可以很轻松地明白右图的业务逻辑。

 ShopController:将这个流程写在service层的queryById方法中,controller层直接调用。

在IShopService类中补充queryById方法,然后在对应的实现类中实现该方法。

从这里进行对比,可以发现缓存命中的速度要快许多。 

 2.店铺类型缓存

 

店铺类型经常用于过滤操作,且店铺类型数据不怎么变化,非常适合做成一个缓存,可以尝试实现一下。

找到controller层这个接口的代码

在加入缓存前,我们要仔细考虑value的数据类型,前面我们遇到的都是一个对象,这里是对象集合。可以用string(因为一切java对象都能转换为json字符串),也可以用list。

 三、缓存更新策略

1.原理 

如前面所说缓存在数据一致性问题 ,为了解决这个问题,以下介绍三种缓存更新策略。

使用哪种方法就看具体的业务需求。

低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存(数据变化小)。

高一致性需求:主动 更新,并以超时剔除作为兜底方案。例如店铺想请查询的缓存。  

主动更新的业务实现在企业里有三种常见模式——

①cache aside:由缓存的调用者,在更新数据库的同时更新缓存。

②read/write through:将缓存与数据库整合成一个服务,同时处理缓存和数据库来保证两者的处理同时成功和失败,由这个服务来保证一致性。调用者无需关心缓存一致性问题。

③write behind caching:调用者只操作缓存(缓存里是新数据,数据库里是旧数据),由其他线程异步地将缓存数据持久化到数据库。该模型的好处是效率得到提升,但要实时监控缓存中数据的变更,且一致性和可靠性有问题(缓存中的数据改变了很多次,但还没有触发异步的更新,这段时间内缓存数据和数据库完全不一致,而且如果缓存出现了宕机,这段数据就完全丢失)

后两种实现较为复杂,而且也找不到比较好的第三方组件,所以第一种方案在企业里用得最多。 cache aside需要开发者进行编码,在编码前需要考虑几个问题。

通过下面两幅图来看看是先删缓存还是先操作数据库。 

 总结

2.实践 

既然了解了缓存更新策略,那么现在我们就来进行实现,读操作在添加商铺缓存时已经写好了,我们只需加个过期时间就行。

接下来主要来看看写操作。

 修改ShopController中的更新接口

实现updateshop方法 

 

四、缓存穿透

1.原理 

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

不怀好意的人就通过无数的线程并发地来向这个不存在的数据发起请求,所有的请求都会到达数据库,很有可能把数据库搞垮

企业中常见的解决方案有两种:

缓存空对象:数据不存在,缓存空对象。优点:实现简单,维护方便。缺点:额外的内存消耗(设TTL解决),可能造成短期的不一致(插入数据后这个数据存在了但缓存中是个null,要么插入时更新缓存,要么不在意短期的不一致等待TTL)

布隆过滤 :在客户端与redis之间加入一层拦截,请求通过布隆过滤器判断数据存在吗,不存在则直接拒绝,存在则放行,剩下的逻辑和之前一样了。优点:内存占用较少,没有多余key。缺点:实现复杂,存在误判可能。

布隆过滤器原理:一个存储二进制位哈希值的byte数组(基于某种哈希算法计算出数据的哈希值,再将哈希值转化为二进制位保存到过滤器),通过判断对应的位置是0还是1来确定数据是否存在,这种存在与否是一种概率上的统计,并不是百分百准确。

一般使用第一种方案。 

2.实践

动手实践,解决查询商铺的业务的缓存穿透问题。我们使用缓存空对象的方法,那么之前的业务逻辑需要小小改变一下。

代码改动如下 

 缓存空对象和布隆过滤都是被动的方案,我们可以主动采取一些措施去解决——增加id的复杂度,避免被猜测id规律;做好数据的基础格式校验;加强用户权限校验等

五、缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效(TTL到期)或Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:给不同的key的TTL添加随机值(TTL+随机值);利用redis集群提高服务的可用性(防宕机);给缓存业务添加降级限流策略;给业务添加多级缓存。

六、缓存击穿

1.原理

缓存击穿问题也叫热点Key问题,就是一个被高并发访问(大量请求)并且缓存重建业务较复杂(如很多表关联运算,耗时长)的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有

①互斥锁

重建缓存这段时间内,其他并发线程都串行执行或相互等待,性能低,于是提出逻辑过期。

②逻辑过期

缓存击穿是因为TTL失效,所以不设置TTL,而是在value中加一个expire逻辑过期时间的字段。

 ③对比

这两种方案本质就是解决缓存重建这段时间里产生的并发问题。互斥锁确保了数据一致性,但牺牲了服务的可用性(性能下降,在阻塞过程中可能不可用)。逻辑过期确保可用性,牺牲了一致性。

2.实践—互斥锁

让我们使用互斥锁来优化商铺缓存,防止缓存击穿。 

这里的锁不是平常使用的synchronize、lock,如果拿不到锁就一直等待,而这里拿到锁和不拿到锁的执行逻辑需自定义,所以要自定义锁

怎么达到互斥锁的效果?redis中string类型的setnx命令可以达到。

 由此可编写获取锁和释放锁的代码

将之前的商铺缓存代码包装起来。

 新建一个加入互斥锁的商铺缓存queryWithMutex

代码完成后可以通过JMeter来进行压力测试。

3.时间—逻辑过期

 修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题。

理论上讲不会出现缓存未命中的情况,因为热点key都会提前添加好,不会过期,直至活动结束人工删除。我们也写一下未命中的处理,直接返回空。

 shop类中没有expire这个字段,若直接添加字段会修改源代码,有两种方案——

①创建一个类继承shop类,再加上expire字段。

②使用装饰模式代替继承。

然后我们来写保存方法。

这里可以进行单元测试看看保存代码是否正确,也相当于提前添加好key

查看redis中是否存在了数据 

 然后继续写使用逻辑过期来解决缓存击穿,主要逻辑和上图一样,能明白图中的逻辑基本没问题

最后可以修改数据库中的数据,使用JMeter来测试一下,前面的一些请求还是旧数据,后面的请求是修改后的新数据则说明没有问题了。

六、缓存工具封装

到这里,我们可以发现缓存穿透和缓存击穿的逻辑性比较复杂,为了降低成本,封装缓存工具。

方法1和3为一组解决缓存穿透问题。方法2和4为一组解决缓存击穿问题。

具体操作如下:

①新建一个CacheClient工具类

②实现方法1和3 

方法1和方法3编码好后,直接使用工具类来解决缓存穿透。

 搜索localhost:8081/shop/0可以发现数据库只进行了一次查询

在缓存中的空对象未过期前都只查一次。

 ③同理实现方法2和4

把这个key提前存入。 

 七、缓存总结

 

;