🎉欢迎来系统设计专栏:JAVA如何实现本地缓存?
📜其他专栏:java面试?数据结构?源码解读?故障分析?Sping
🎬作者简介:大家好,我是小徐🥇
??博客首页:CSDN主页小徐的博客
🌄每日一句:好学而不勤非真好学者📜 欢迎大家关注! ??
本地缓存和分布式缓存是两种不同的缓存架构,它们的主要区别在于数据的存储和管理方式。
本地缓存是指将数据存储在单个应用程序的内存中,它通常被用于提高应用程序的性能,减少对数据库等后端存储系统的请求次数。
本地缓存的优点是速度快、易于使用和管理,但是它只能在应用程序的本地节点使用,不能跨多个节点进行共享。也就是说,本地缓存在集群环境中,会存在不一致的问题。多个本地缓存之间的数据可能不一致。
分布式缓存是指将数据存储在多个节点的内存中,这些节点可以在不同的服务器上,甚至在不同的地理位置上。
分布式缓存的优点是可以支持多个应用程序共享数据,提高系统的可伸缩性和可用性,但是它的管理和维护成本较高,需要考虑数据一致性和故障恢复等问题。
总的来说,本地缓存适合于单个应用程序的性能优化,而分布式缓存则适合于多个应用程序共享数据、提高系统可伸缩性和可用性的场景。
在设计本地缓存时,一般需要考虑以下几个方面的问题
般来讲,为了提升缓存的效率,通常采用Key-Value结构进行数据存储,也就是说,缓存中的数据保存和读取都需要有一个Key,通过Key来读取固定的缓存的Value。
本地缓存一定要考虑线程安全的问题,因为大多数情况下本地缓存都是一个全局可访问的变量,那么就会有多个线程同时访问,所以线程安全问题不容忽视
因为是本地缓存,而本地内存中的数据是要占用JVM的堆内存的,所以内存是有上限要求的,如果无限存储,最终一-定会导致OOM的问题。
为了避免OOM的问题,一般会考虑在缓存中增加清除策略,通过一定的手段定期的清理掉一些数据,来保证内存占用不会过大,常见清除策略主要有有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等:
有了清除策略并不能保证百分百的可以删除数据,极端情况会会使得某些数据一直无法删除。这时候就需要有一种机制,能够保证某些K-V一定可以删除。通常采用的方案是给每一个缓存的key设置过期时间,当达到过期时间之后直接删除,采用清除策略+过期时间双重保证
考虑到以上这些问题之后,就可以考虑如何具体实现一个本地缓存了。
最简单的方式是通过HashMap来实现一个本地缓存,因为他本身就是一种Key-Value结构的,并且如果使用ConcurrentHashMap的话,也能保证线程安全,不过需要自己实现对象上限、过期策略以及清除策略
除此之外,也有一些比较成熟的开源的本地缓存框架可以直接使用,比较常用的有:
推荐优先使用Caffeine作为本地缓存,在功能上,GuavaCache支持的功能,Caffeine都支持,另外Caffeine支持异步Cache和写入外部资源,这两个Guava Cache是不支持的。Caffeine也是Spring 5中默认支持的Cache。而Caffeine在性能上要比GuavaCache好很多主要有以下几个原因
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 本地缓存工具
*/
/**
* @Author xiaoxu
* @Date 2024/1/20 22:50
* @description:本地缓存工具
*/
@Component
public class LocalCacheManager implements InitializingBean {
private Cache<String, String> localCache;
/**
* 向缓存中保存数据,如果已经存在则不覆盖
*
* @param key
* @param value
*/
public void putIfNotExist(String key, String value) {
if (localCache.getIfPresent(key) == null) {
localCache.put(key, value);
}
}
/*
* 根据key获取缓存数据头
*/
public String get(string key) {
return localCache.getIfPresent(key);
}
public void del(String key) {
localCache.invalidate(key);
}
/**
* 在bean初始化时,初始化本地缓存
*/
@Override
public void afterPropertiesSet() {
localCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit.SECONDS)
.maximumSize(1)
.build();
}
}
想要让本地缓存保持一致性,那么也就意味着,当一台机器上的本地缓存更新或者失效时,别的机器也要感知到,并且能及时处理。
首先,有一个方案,就是我们给本地缓存记录一个版本,当某台机器的本地缓存更新之后把他的最新的版本号以及缓存中的数据记录到数据库中,这样,当下次有请求请求到一个未更新过本地缓存的机器时,对比一下版本号,发现版本号旧了,那么就从数据库中更新一下本地缓存。
其次,当某一个机器上的本地缓存发生变更之后,向配置中心做一次配置变更,然后通过配置中心把变更再推送到每一台机器上,大家监听配置变化做本地缓存的更新。
除了数据库和配置中心,也可以用redis,但是,这就有点脱裤子放屁了,用了本地缓存,还要去检查redis和数据库。。。
其实,真正的工作中,一般来说,对于本地缓存的使用,一般都是这样的
首先,肯定是要评估数据的变化频率,对于变化不频繁的数据,才会考虑放到本地缓存中。那种频繁更新的数据,其实并不适合放到本地缓存。比如数据库的库存,你见过哪个公司秒杀是在本地缓存做的?本地缓存这么快,咋不用呢? 因为他就不适合啊。
还有就是,要提前评估下业务上能否接收不一致,以及能接受的不一致的时长。如果接受不了 不一致,那就绝对不能用本地缓存。
如果能接受,那么就基于业务上能接受的时长设置失效时长,比如业务上可以接受10分钟的延迟,那么我们可以设置个8分钟的超时时间。这样到期之后这个缓存的内容就会自动失效。
在初始化缓存的时候,可以设置参数,如expireAfterAccess、expireAfterWrite.
refreshAfterWrite,利用这些参数我们可以配置自动更新及自动失效。
在自动失效后,查询本地缓存就会有一次cache miss,然后下次再查询就会去分布式缓存查询,然后再缓存到本地缓存中即可。这样就能保持最新数据了。
这是让缓存自动失效的方式,还有一种可以让本地缓存自动更新的方式。如Cffeine就支持可以定义一个refresh策略,他会定时的进行数据的刷新。
以上,会在达到缓存刷新的时间后,Caffiene会自动调用load方法进行数据读取并更新。
仅供参考,欢迎评论区留言,一起讨论~
?
?