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算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被淘汰。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下: 新数据插入到链表头部; 每当缓存命中,则将数据移到链表头部; 当链表满的时候,将链表尾部的数据丢弃。 LFU LFU(Least Frequently Used ,最少频次使用)也是一种常见的缓存算法。 顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。 LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。 具体实现如下: 新加入数据插入到队列尾部(因为引用计数为1); 队列中的数据被访问后,引用计数增加,队列重新排序; 当需要淘汰数据时,将已经排序的列表最后的数据块删除。

March 22, 2026 · 1 min · santu

你知道哪些缓存失效算法?

缓存失效算法主要是进行缓存失效的,当缓存中的存储的对象过多时,需要通过一定的算法选择出需要被淘汰的对象,一个好的算法对缓存的命中率影响是巨大的。常见的缓存失效算法有FIFO、LRU、LFU,以及Caffeine中的Window TinyLFU算法。 FIFO FIFO 算法是一种比较容易实现也最容易理解的算法。它的主要思想就是和队列是一样的,即先进先出(First In First Out) 一般认为一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小。 因为FIFO刚好符合队列的特性,所以通常FIFO的算法都是使用队列来实现的: 新数据插入到队列尾部,数据在队列中顺序移动; 淘汰队列头部的数据; LRU&LFU ✅LRU 和 LFU 有啥区别? W-TinyLFU LFU 通常能带来最佳的缓存命中率,但 LFU 有两个缺点: 它需要给每个记录项维护频率信息,每次访问都需要更新,需要一个巨大的空间记录所有出现过的 key 和其对应的频次; 如果数据访问模式随时间有变,LFU 的频率信息无法随之变化,因此早先频繁访问的记录可能会占据缓存,而后期访问较多的记录则无法被命中; 如果一个刚加入缓存的元素,它的频率并不高,那么它可能会会直接被淘汰。 其中第一点过于致命导致我们通常不会使用 LFU。我们最常用的 LRU 实现简单,内存占用低,但其并不能反馈访问频率。LFU 通常需要较大的空间才能保证较好的缓存命中率。 W-TinyLFU是一种高效的缓存淘汰算法,它是TinyLFU算法的一种改进版本,主要用于处理大规模缓存系统中的淘汰问题。W-TinyLFU的核心思想是基于窗口的近似最少使用算法,即根据数据的访问模式动态地调整缓存中数据的淘汰策略。W-TinyLFU 综合了LRU和LFU的长处:高命中率、低内存占用。 W-TinyLFU由多个部分组合而成,包括窗口缓存、过滤器和主缓存。 使用LRU来作为一个窗口缓存,主要是让元素能够有机会在窗口缓存中去积累它的频率,避免因为频率很低而直接被淘汰。 主缓存是使用SLRU,元素刚进入W-TinyLFU会在窗口缓存暂留一会,被挤出窗口缓存时,会在过滤器中和主缓存中最容易被淘汰的元素进行PK,如果频率大于主缓存中这个最容易被淘汰的元素,才能进入主缓存。

March 22, 2026 · 1 min · santu

如何保证本地缓存的一致性?

典型回答 ✅本地缓存和分布式缓存有什么区别? 我们知道,本地缓存和分布式缓存相比有很多优点,但是最大的缺点就是存在着一致性的问题。于是就衍生出这个问题:如何保证本地缓存的一致性。 在回答这个问题之前,我先多说几句,这个问题挺好的,可以激发大家的解决问题的想法。但是后面我要说的不管哪个方案,如果真的是用在项目中,那么一定是不合理的。 因为本地缓存就是通过牺牲一致性来提升效率的,如果他能保证一致性,那么也就没分布式缓存什么事儿了。 虽然我们可以用接下来要介绍的手段在一定程度上解决,但是我要强调的是,很多方案你做了之后,本地缓存自身的优势可能也就没了。 所以,如果有一致性要求,那么就不要用本地缓存!!! 想要让本地缓存保持一致性,那么也就意味着,当一台机器上的本地缓存更新或者失效时,别的机器也要感知到,并且能及时处理。 首先可以考虑,借助配置中心。当某一个机器上的本地缓存发生变更之后,向配置中心做一次配置变更,然后通过配置中心把变更再推送到每一台机器上,大家监听配置变化做本地缓存的更新。 另外,借助MQ的广播消息,可以实现这个功能,当有实例需要更新本地缓存的时候,发一个MQ的广播消息,然后所有实例监听到这个广播消息后,各自更新自己的本地缓存。 ✅RocketMQ怎么实现消息分发的? 除了数据库、广播消息和配置中心,也可以用redis,但是,这就有点脱裤子放屁了,用了本地缓存,还要去检查redis和数据库。。。 其实,真正的工作中,一般来说,对于本地缓存的使用,一般都是这样的: 首先,肯定是要评估数据的变化频率,对于变化不频繁的数据,才会考虑放到本地缓存中。那种频繁更新的数据,其实并不适合放到本地缓存。比如数据库的库存,你见过哪个公司秒杀是在本地缓存做的?本地缓存这么快,咋不用呢?因为他就不适合啊。 还有就是,要提前评估下业务上能否接收不一致,以及能接受的不一致的时长。如果接受不了不一致,那就绝对不能用本地缓存。 如果能接受,那么就基于业务上能接受的时长设置失效时长,比如业务上可以接受10分钟的延迟,那么我们可以设置个8分钟的超时时间。这样到期之后这个缓存的内容就会自动失效。 在初始化缓存的时候,可以设置参数,如expireAfterAccess、expireAfterWrite、refreshAfterWrite,利用这些参数我们可以配置自动更新及自动失效。 自动失效: 1 2 3 Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) // 设置缓存项写入后的过期时间为5秒 .build(); 在自动失效后,查询本地缓存就会有一次cache miss,然后下次再查询就会去分布式缓存查询,然后再缓存到本地缓存中即可。这样就能保持最新数据了。 这是让缓存自动失效的方式,还有一种可以让本地缓存自动更新的方式。如Cffeine就支持可以定义一个refresh策略,他会定时的进行数据的刷新。 自动更新: 1 2 3 4 5 6 7 8 Cache<String, String> cache = Caffeine.newBuilder() .refreshAfterWrite(5, TimeUnit.SECONDS) // 设置缓存项写入后的自动刷新时间为5秒 .build(new CacheLoader<String, String>() { //定义一个CacheLoader,实现load方法。 @Override public ListenableFuture<String> reload(String key, String oldValue) throws Exception { return remoteCache.get(key); } }); 以上,会在达到缓存刷新的时间后,Caffeine会自动调用load方法进行数据读取并更新。 ...

March 22, 2026 · 1 min · santu

如何实现多级缓存?

典型回答 ✅说一说多级缓存是如何应用的? 本文重点说一说在Java应用中,多级缓存如何实现。 多级缓存是比较常见的一种性能优化的手段,一般来说就是本地缓存+分布式缓存。 本地缓存一般采用Caffeine和Guava,这两种是性能比较高的本地缓存的框架。他们都提供了缓存的过期、管理等功能。 分布式缓存一般采用Redis、Memcached等分布式缓存框架。 在做多级缓存的方案中,会先查询本地缓存,如果本地缓存查不到,再查询分布式缓存。并且在分布式缓存中查询到之后保存到本地缓存中一份。 有些特殊场景,如黑名单场景,本地缓存也会用bloom filter来充当,因为bloom filter是有假阳性的特性的,所以命中后需要在查一次分布式缓存,如果没命中则直接返回。 简单代码逻辑如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; } 扩展知识 一致性问题 使用多级缓存,比较大的问题就是一致性如何保证,因为用到了本地缓存,而一个集群中有很多台服务器,每个服务器上面的本地缓存内容都不一样。 ...

March 22, 2026 · 1 min · santu

如何实现本地缓存?

典型回答 所谓本地缓存,就是和应用服务器在一起的缓存工具,将需要缓存的数据放到本地缓存中,可以大大的提升访问速度。 在设计本地缓存时,一般需要考虑以下几个方面的问题: 数据结构 一般来讲,为了提升缓存的效率,通常采用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好很多,主要有以下几个原因: 剔除算法,GuavaCache采用的是「LRU」算法,而Caffeine采用的是「Window TinyLFU」算法,这是两者之间最大,也是根本的区别。 立即失效,Guava会把立即失效 (例如:expireAfterAccess(0) and expireAfterWrite(0)) 转成设置最大Size为0。这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffeine能正确识别这种剔除原因。 取代提醒,Guava只要数据被替换,不管什么原因,都会触发剔除监听器。而Caffeine在取代值和先前值的引用完全一样时不会触发监听器。 异步化,Caffeine的很多工作都是交给线程池去做的(默认:ForkJoinPool.commonPool()),例如:剔除监听器,刷新机制,维护工作等。 介绍下LFU、LRU等缓存失效算法? 扩展知识 基于Caffeine实现本地缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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 hollis */ @Component public class LocalCacheManager implements InitializingBean { private Cache<String, String> localCache; /** * 向缓存中保存数据,如果已经存在则不覆盖 */ 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(); } }

March 22, 2026 · 1 min · santu

本地缓存和分布式缓存有什么区别?

典型回答 本地缓存和分布式缓存是两种不同的缓存架构,它们的主要区别在于数据的存储和管理方式。 **本地缓存是指将数据存储在单个应用程序的内存中,**它通常被用于提高应用程序的性能,减少对数据库等后端存储系统的请求次数。 本地缓存的优点是速度快、易于使用和管理,但是它只能在应用程序的本地节点使用,不能跨多个节点进行共享。也就是说,本地缓存在集群环境中,会存在不一致的问题。多个本地缓存之间的数据可能不一致。 分布式缓存是指将数据存储在多个节点的内存中,这些节点可以在不同的服务器上,甚至在不同的地理位置上。 分布式缓存的优点是可以支持多个应用程序共享数据,提高系统的可伸缩性和可用性,但是它的管理和维护成本较高,需要考虑数据一致性和故障恢复等问题。 总的来说,本地缓存适合于单个应用程序的性能优化,而分布式缓存则适合于多个应用程序共享数据、提高系统可伸缩性和可用性的场景。 扩展知识 本地缓存的实现方式 ✅如何实现本地缓存? 什么是近端缓存? **近端缓存(Edge Cache)通常是指位于网络边缘、离用户更近的位置的缓存。**它可以用于在网络上尽可能快地向用户提供内容,减少用户请求的响应时间和带宽占用。 离用户更近的位置,那么首先能想到的就是CDN,其实它就是一种使用近端缓存的技术,它将内容存储在分布在全球各地的缓存服务器上,以提供更快速和可靠的内容传输服务。 除了CDN以外,应用服务器相比于分布式缓存,离用户也更近一些。所以和应用服务器部署在一起的缓存有时候可以叫做是近端缓存。 比如我们前面提到的本地缓存,他也是近端缓存的一种。 还有一种近端缓存,就是可以把Redis等这种分布式缓存在应用服务器上也部署一份,这样就使得查询缓存的时候不需要网络通信的远程调用,也能提升查询的速度。

March 22, 2026 · 1 min · santu

如何实现多级缓存的一致性?

典型回答 当被问到这个问题的时候,有两个情况,一种是面试官想问你关于操作系统的CPU的缓存中的L1\L2\L3的一致性问题,一种是他想问你本地缓存、Redis这种分布式场景下的多级缓存的一致性问题。所以,你要结合语境,分析他想问那个,如果不清楚,大概率是分布式系统的这种一致性。或者你问他一下,想问的是哪个。 如果是CPU的多级缓存的一致性问题,可以直接回答MESI就行了。这里就不展开了。 ✅什么是MESI缓存一致性协议 那么如果是分布式场景下的多级缓存,那么一致性问题如何解决呢? 解决多级缓存一致性的核心思想,一定要记住:“放弃强一致性,追求最终一致性”。不可能追求强一致性的!如果真的要这么强的一致性要求,那干脆就不要用多级缓存了,直接分布式缓存,或者数据库就行了。 本地缓存本身就一种牺牲一致性,换区性能的方案了(CAP中选了AP,放弃了CP),不可能做到同时能满足强一致性和性能要求的。 那么,如何保证本地缓存和分布式缓存,我们就拿Caffeine和Redis来举例,其实方案和我们之前讲过的Redis和数据库的一致性大差不差,因为他们要解决的问题是一样的。 ✅如何解决Redis和数据库的一致性问题? 和Redis数据库一致性方案的区别 本地缓存和分布式缓存的一致性方案,和,Redis和数据库一致性方案,之间的区别主要有这么几个: 1、一般来说,我们的本地缓存的超时时间会设置的比较短,一般都会借助框架的自动超时和自动刷新的机制。所以相对来时比Redis和数据库的一致性保障中的容错率更高一些,即不太需要用延迟双删的方案。 2、分布式缓存只需要做一次操作就行了,就算是集群的,他也有同步的机制,但是本地缓存是默认无同步机制的,需要自己考虑多个本地缓存之间的一致性问题。 方案1、先更新Redis缓存,再删除本地缓存(常用) 这就是一种最简单的方案了,如果有需要更新缓存的时候,先去更新Redis缓存,成功之后再把本地缓存失效掉。 写请求到达,先更新Redis。 删除本地缓存中对应的数据。 通过某种广播机制,通知集群中的所有其他节点,删除其本地缓存 (L1) 中对应的数据。 这里所谓的广播机制可以借助配置中心或者MQ的广播消息实现,具体可以参考: ✅如何保证本地缓存的一致性? 这里为什么不是更新本地缓存而是要删除本地缓存,这个和Redis数据库一致性中介绍的原因是一样的。 ✅如何解决Redis和数据库的一致性问题? 方案2、基于Canal异步失效缓存(大厂常用) 这是一个非常优雅且对业务代码侵入性极小的方案。Canal 模拟 MySQL Slave,监听数据库的 binlog。当有数据变更时,Canal 可以从 binlog 中解析出变更的数据和表名。Canal 将变更信息发送给 MQ。所有的应用节点消费 MQ 中的消息,解析出哪些数据发生了变更,然后同时删除 Redis 中的数据和自己的本地缓存。 这个方案我们在介绍Redis数据库一致性的时候就提过,比较常见的方案,叫做Cache-Aside,好处就是同步链路上不需要操作缓存,只需要操作数据库就行了, 其他的靠binlog监听的方式异步保证。 而再加上本地缓存之后,就还有个好处了,那就是天然就可以借助MQ的广播机制,来实现多个节点上的本地缓存的一致性删除了。 但是,这个方案有个关键的限制,那就是一定要依赖数据库,如果是那种单纯是本地缓存+分布式缓存的存储架构,就不适合这种方案了。可以用下面的方案。 方案3,借助Redis的事件Pub/Sub机制(复杂,不建议) 借助 Redis 的事件通知(Keyspace Notifications)+ Pub/Sub 确实能够帮助实现多级缓存一致性 Redis 提供了 Keyspace Notifications 功能,可以对数据库中某些事件(如 key 被修改、过期、删除)发布消息,客户端通过 Pub/Sub 订阅相应的频道来感知。简单的流程如下: 1、应用删除/更新 Redis 中的缓存; 2、Redis 触发事件通知(如 del 或 set); 3、订阅该事件的应用实例清理/更新自己的本地缓存。 ...

March 22, 2026 · 2 min · santu

留言给博主