【全网最新最全】一篇文章带你了解“本地缓存”

目录

一、本地缓存与分布式缓存的区别

二、什么是近端缓存?

三、实现本地缓存

3.1 实现需要

3.2 基于Caffeine实现本地缓存

四、缓存失效算法

4.1 FIFO

4.2 LRU&LFU

LRU

LFU

五、如何实现多级缓存?

六、如何保证本地缓存的一致性问题?


一、本地缓存与分布式缓存的区别

本地缓存和分布式缓存是两种不同的缓存架构,它们的主要区别在于数据的存储和管理方式。

本地缓存是指将数据存储在单个应用程序的内存中,它通常被用于提高应用程序的性能,减少对数据库等后端存储系统的请求次数。

本地缓存的优点是速度快、易于使用和管理,但是它只能在应用程序的本地节点使用,不能跨多个节点进行共享。也就是说,本地缓存在集群环境中,会存在不一致的问题。多个本地缓存之间的数据可能不一致。

分布式缓存是指将数据存储在多个节点的内存中,这些节点可以在不同的服务器上,甚至在不同的地理位置上。

分布式缓存的优点是可以支持多个应用程序共享数据,提高系统的可伸缩性和可用性,但是它的管理和维护成本较高,需要考虑数据一致性和故障恢复等问题。

总的来说,本地缓存适合于单个应用程序的性能优化,而分布式缓存则适合于多个应用程序共享数据、提高系统可伸缩性和可用性的场景。

二、什么是近端缓存?

近端缓存(Edge Cache)通常是指位于网络边缘、离用户更近的位置的缓存。它可以用于在网络上尽可能快地向用户提供内容,减少用户请求的响应时间和带宽占用。

离用户更近的位置,那么首先能想到的就是CDN,其实它就是一种使用近端缓存的技术,它将内容存储在分布在全球各地的缓存服务器上,以提供更快速和可靠的内容传输服务。

除了CDN以外,应用服务器相比于分布式缓存,离用户也更近一些。所以和应用服务器部署在一起的缓存有时候可以叫做是近端缓存。

比如我们前面提到的本地缓存,他也是近端缓存的一种。

还有一种近端缓存,就是可以把Redis等这种分布式缓存在应用服务器上也部署一份,这样就使得查询缓存的时候不需要网络通信的远程调用,也能提升查询的速度。

三、实现本地缓存

3.1 实现需要

在设计本地缓存时,一般需要考虑以下几个方面的问题:

数据结构

一般来讲,为了提升缓存的效率,通常采用Key-Value结构进行数据存储,也就是说,缓存中的数据保存和读取都需要有一个Key,通过Key来读取固定的缓存的Value。

线程安全

本地缓存一定要考虑线程安全的问题,因为大多数情况下本地缓存都是一个全局可访问的变量,那么就会有多个线程同时访问,所以线程安全问题不容忽视。

对象上限

因为是本地缓存,而本地内存中的数据是要占用JVM的堆内存的,所以内存是有上限要求的,如果无限存储,最终一定会导致OOM的问题。

清除策略

为了避免OOM的问题,一般会考虑在缓存中增加清除策略,通过一定的手段定期的清理掉一些数据,来保证内存占用不会过大,常见清除策略主要有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等。

过期时间

有了清除策略并不能保证百分百的可以删除数据,极端情况会会使得某些数据一直无法删除。这时候就需要有一种机制,能够保证某些K-V一定可以删除。通常采用的方案是给每一个缓存的key设置过期时间,当达到过期时间之后直接删除,采用清除策略+过期时间双重保证。

考虑到以上这些问题之后,就可以考虑如何具体实现一个本地缓存了。

最简单的方式是通过HashMap来实现一个本地缓存,因为他本身就是一种Key-Value结构的,并且如果使用ConcurrentHashMap的话,也能保证线程安全,不过需要自己实现对象上限、过期策略以及清除策略。

除此之外,也有一些比较成熟的开源的本地缓存框架可以直接使用,比较常用的有:

  • Guava Cache
  • Caffeine(推荐)
  • Encache

推荐优先使用Caffeine作为本地缓存,在功能上,GuavaCache支持的功能,Caffeine都支持,另外Caffeine支持异步Cache和写入外部资源,这两个Guava Cache是不支持的。Caffeine也是Spring 5中默认支持的Cache。而Caffeine在性能上要比GuavaCache好很多,主要有以下几个原因:

  1. 剔除算法,GuavaCache采用的是「LRU」算法,而Caffeine采用的是「Window TinyLFU」算法,这是两者之间最大,也是根本的区别。
  2. 立即失效,Guava会把立即失效(例如:expireAfterAccess(0) and expireAfterWrite(0))转成设置最大Size为0,这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffeine能正确识别这种剔除原因。
  3. 取代提醒,Guava只要数据被替换,不管什么原因,都会触发剔除监听器。而Caffeine在取代值和先前值的引用完全一样时不会触发监听器。
  4. 异步化,Caffeine的很多工作都是交给线程池去做的(默认:ForkJoinPool.commonPool()),例如:剔除监听器,刷新机制,维护工作等。

3.2 基于Caffeine实现本地缓存

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;

/**
 * 本地缓存工具
 */
@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获取缓存数据
     * 
     * @param 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(10, TimeUnit.SECONDS)
                .maximumSize(1000)
                .build();
    }
}

四、缓存失效算法

缓存失效算法主要是进行缓存失效的,当缓存中的存储的对象过多,需要通过一定的算法选择出需要被淘汰的对象,一个好的算法对缓存的命中影响是巨大的。常见的缓存失效算法有FIFO、LRU、LFU,以及Caffeine中的Window TinyLFU算法。

4.1 FIFO

FIFO算法是一种比较容易实现也最容易理解的算法。它的主要思想就是和队列是一样的,即先进先出(First In First Out)。

一般认为一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小。

因为FIFO刚好符合队列的特性,所以通常FIFO的算法都是使用队列来实现的。

  1. 新数据插入到队列尾部,数据在队列中顺序移动;
  2. 淘汰队列头部的数据;

4.2 LRU&LFU

LRU(The Least Recently Used),最近最少使用,最久未被访问的数据会被最早淘汰。

LFU(Least Frequently Used),最少频次使用,访问次数最少的数据会被最早淘汰。

LRU只关注数据最近一次被使用时间(时间越久,越容易被淘汰),LFU只关注数据最近一段时间被使用的次数(次数越少,越容易被淘汰)。

假设缓存数据使用顺序如下(从左到右表示时间从晚到早):

A、B、C、D、A、C、C、D

那么,如果按照LRU淘汰的是,就淘汰那个最近一次使用时间更早的,即D。

如果按照LFU淘汰的是,就淘汰那个使用次数最少的,即B。

LRU

LRU(The Least Recently Used,最近最少使用)是一种常见的缓存算法,在很多分布式缓存系统(如Redis, Memcached)中都有广泛使用。

LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被淘汰。

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

  1. 新数据插入到链表头部;
  2. 每当缓存命中,则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。

LFU

LFU(Least Frequently Used,最少频次使用)也是一种常见的缓存算法。

顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。

LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。

具体实现如下:

  1. 新加入数据插入到队列尾部(因为引用计数为1);
  2. 队列中的数据被访问后,引用计数增加,队列重新排序;
  3. 当需要淘汰数据时,将已经排序的列表最后的数据块删除。

五、如何实现多级缓存?

多级缓存是比较常见的一种性能优化的手段,一般来说就是本地缓存 + 分布式缓存。

本地缓存一般采用Caffeine和Guava,这两种是性能比较高的本地缓存的框架。他们都提供了缓存的过期、管理等功能。

Caffeine和Guava,这两种是性能比较高的本地缓存的框架。他们都提供了缓存的过期、管理等功能。

分布式缓存一般采用Redis、Memcached等分布式缓存框架。

在做多级缓存的方案中,会先查询本地缓存,如果本地缓存查不到,再查询分布式缓存。并且在分布式缓存中查询到之后保存到本地缓存中一份。

有些特殊场景,如黑名单场景,本地缓存也会用bloom filter来充当,因为bloom filter是有假阳性的特性的,所以命中后需要再查一次分布式缓存,如果没命中则直接返回。 

简单代码逻辑如下:

public String query(String key) {
    String localResult = localCache.get(key);
    if (localResult == null) {
        String remoteResult = remoteCache.get(key);
        if (remoteResult != null) {
            localCache.put(remoteResult);
        }
    }
    return localResult;
}

六、如何保证本地缓存的一致性问题?

使用多级缓存,比较大的问题就是一致性如何保证,因为用到了本地缓存,而一个集群中有很多台服务器,每个服务器上面的本地缓存内容都不一样,这就是使用本地缓存最大的劣势。

其实这个问题压根就不合理!!!

因为本地缓存就是通过牺牲一致性来提升效率的,如果他能保证一致性,那么也就没分布式缓存什么事儿了。

虽然我们可以用一定的手段在一定程度上解决,但是我要强调的是,很多方案你做了之后,本地缓存自身的优势可能也就没了。

所以,如果有一致性要求,那么就不要用本地缓存!!!

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全真王重阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值