一、 背景
(近期测试,因为本地缓存的引起了一个线上问题,决定重新梳理一下本地缓存的知识点) 大家在测试的时候,必然遇到过这样的问题,我们在后台管理系统做了编辑更新操作之后发现,为什么C端接口返回的数据还是更新之前的数据?这个时候我们第一步会先去查看数据库中的数据,如果数据库中已经是最新的数据,那接下来我们会再去查看redis中的数据,那如果redis里已经是最新的数据或者redis里根本就没有缓存呢?一般这种情况下,我们就要和研发确认一下这个场景下的数据,是否有使用本地缓存了。那么究竟什么是本地缓存?为什么要使用本地缓存?什么场景下适合用本地缓存呢?在接下来的内容中,我们可以一起探讨。
二、 什么是本地缓存
和本地缓存一起提及的,还有另一种是分布式缓存。分布式缓存在日常测试中,大家可能会更熟悉些。这边以一个电商商品系统作为例子,商品系统给C端提供的接口,基本上都会使用redis来作为数据库查询前面的一层数据缓存,redis就是分布式缓存的一种实现。Redis是独立于后端服务之外部署的单独的实例,后端服务的不同机器可以访问同一个redis实例,缓存数据通过网络进行传输,类似Redis这种不同机器可以共享缓存的特性的即为分布式缓存。而本地缓存,是和后端服务在同一个进程中,数据读写都是在同一个进程中进行,因为数据都是缓存在服务进程的内存空间中,即使是同一个服务的不同实例,也不可以共享缓存数据。
三、 为什么要使用本地缓存
在说明为什么要使用本地缓存前,我们先看看为什么要使用缓存?不管是商品数据还系统中的其他数据,基本上都是存储在mysql这类关系型数据库中,那我们在使用数据的时候,直接从mysql中查询最新的数据是否就可以了?在业务场景来看,确实没有任何问题,站在技术角度看,如果这是个后台管理系统,或者是个流量很低的系统,那也没啥大问题,但如果是个给终端用户使用的场景,并且有一定并发量的系统,那基本上就是一颗地雷,而且随时都可能引爆。因为mysql等关系型数据库,在设计初衷就不是为高并发所设计的,主要考虑的是可靠性、持久性等因素,所以在高并发下,数据库往往是系统最大的瓶颈。但根 据局部性远离,80%的请求会落在20%的数据上,在读多写少的场景下,增加一层缓存非常有利于提升系统的并发量和稳定性。而商品中心就是这样的系统。 那么既然有了redis这样的缓存中间件,为什么还要本地缓存呢。早期因为流量较少,商品系统也并未使用本地缓存,终端流量进入到后端服务之后先查询redis,如果redis命中了数据,则直接返回,如果redis未命中,则再查询数据库,并将查询结果缓存在redis中。随着业务的逐步发展,商品中心的流量逐步增加,redis实例的使用率也在逐步增加,当流量增加到一定程度的时候,redis就会存在性能的瓶颈。而且由于查询redis本身也需要网络IO,当网络抖动或者redis本身出现故障时,接口性能、业务稳定性也会受到极大的考验。自此,商品中心开始引入本地缓存,作为redis缓存的前置,先查询本地缓存,如果本地缓存未查询到,才进入后续查redis查数据库的流程。
四、 本地缓存适用场景
都说在技术领域没有银弹,虽然本地缓存可以帮助解决一些性能和并发问题,但是并不是所有场景都适合使用本地缓存,那下面就说下在商品这边总结的本地缓存适用场景。 1、 适用于更新不频繁且不要求绝对实时性的数据。由于本地缓存很难像分布式缓存一样实时更新,所以本地缓存的数据具有一定的延时性,当数据发生变更时,本地缓存数据可能会有一定的滞后,造成查询到的数据不一定就是最新数据,所以在使用本地缓存的场景要能接受数据的一定程度的延迟。比如说商品核心数据,类似于商品标题、商品图片等数据,只有商品的运营人员在后台中才会编辑,一个商品录入之后,往往数年时间都不会有运营进行更新,并且当运营更新了标题或者图片,即使C端用户在短暂的时间内看到的是之前的信息,也不会影响到用户体验。而想商品库存、最低价这些信息,由于经常会实时变更,所以就不适合本地缓存。
2、 适用于数据量较少的数据。商品系统中有很多数据,比如说商品收藏数据、商品选品数据,而这些数据也都会在app中被用户访问,那么这些数据是否可以被放入到本地缓存中呢?那么我们还是以收藏数据举例,收藏之前使用的redis内存为128G,那么本地缓存可用的内存呢,收藏服务的机器是16G的内存,那么可想而之,收藏数据量级相对于本地缓存来说,过于庞大。而使用到本地缓存的核心数据呢,假如说一个商品核心信息占用10KB的内存,线上有10w个热销商品,那么这10w热销商品同时缓存在本地缓存中的量级为10*100000/1024,则占用1个G左右的内存,那么本地缓存存放这部分数据,还是绰绰有余的。
五、 本地缓存选型
1、 HashMap实现 本地缓存的本质就是在内存中缓存我们需要的数据,而且也是K-V这种数据结构,那JDK中本身就有HashMap这类K-V的数据结构,所以本地缓存最简单的实现,就是将数据缓存在HashMap中。当然,HashMap是非线程安全的,JDK中也有对应的线程安全的实现,这里就不展开赘述了。HashMap实现简单,如果业务场景简单,那么可以简单使用使用,但是它满足不了复杂的业务需求,所以一般不太会使用。 2、 Guava使用 本地缓存绕不开的工具就是Google的Guava,具体Guava的使用也比较简单,构建出LoadingCache后即可使用。
LoadingCache<String, String> loadingCache //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例 = CacheBuilder.newBuilder() //设置并发级别为8,并发级别是指可以同时写缓存的线程数 .concurrencyLevel(8) //设置写缓存后60秒过期 .expireAfterWrite(60, TimeUnit.SECONDS) //设置写缓存后30秒刷新
.refreshAfterWrite(60, TimeUnit.MINUTES)
//设置缓存容器的初始容量为5
.initialCapacity(5)
//设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(100)
//设置要统计缓存的命中率
.recordStats()
//设置缓存的移除通知
.removalListener(notification -> log.info(notification.getKey() + " 被移除了,原因: " + notification.getCause()))
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
//这里是如果使用loadingCache.get(key)或者这里是如果使用loadingCache.getUnchecked(key)获取内存中数据时,如果为空,则执行次方法并将结果自动会放入LoadingCache内存缓存中,return值就是放入内存缓存中的值
return “具体的查询数据”;
}
});
|