你应该知道的Java缓存进化史

SAP FI 提示没有为会计年度0 定义版本2025 GP626 SAP 错误 GP626 是一个常见的系统消息,通常表示某个会计年度的版本(如版本 0)未被定义或未激活。:在执行某些事务(如创建采购订单、过账凭证或维护成本中心)时,系统会检查相关版本是否已定义。的“Settings for Each Fiscal Year”界面中,选择一个已激活的会计年度。的“Settings for Each Fiscal Year”界面中,找到未激活的会计年度。:在控制范围(Controlling Area)中,未为指定的会计年度定义版本 0。如果没有,添加或复制现有年度的设置。 阅读详情

“

本文主要讲述爱奇艺的缓存之路和本地缓存的一个发展历史,以及每一种缓存的实现基本原理。
在这里插入图片描述

背景

本文是上周去技术沙龙听了一下爱奇艺的 Java 缓存之路有感写出来的。先简单介绍一下爱奇艺的 Java 缓存道路的发展吧。
在这里插入图片描述
可以看见图中分为几个阶段:

第一阶段:数据同步加 Redis
欢迎工作一到五年的Java工程师朋友们加入Java技术交流:611481448
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

通过消息队列进行数据同步至 Redis,然后 Java 应用直接去取缓存。这个阶段的优点是:由于是使用的分布式缓存,所以数据更新快。缺点也比较明显:依赖 Redis 的稳定性,一旦 Redis 挂了,整个缓存系统不可用,造成缓存雪崩,所有请求打到 DB。

第二,三阶段:JavaMap 到 Guava Cache
这个阶段使用进程内缓存作为一级缓存,Redis 作为二级。优点:不受外部系统影响,其他系统挂了,依然能使用。缺点:进程内缓存无法像分布式缓存那样做到实时更新。由于 Java 内存有限,必定缓存得设置大小,然后有些缓存会被淘汰,就会有命中率的问题。

第四阶段: Guava Cache 刷新
为了解决上面的问题,利用 Guava Cache 可以设置写后刷新时间,进行刷新。解决了一直不更新的问题,但是依然没有解决实时刷新。

第五阶段:外部缓存异步刷新

在这里插入图片描述

这个阶段扩展了 Guava Cache,利用 Redis 作为消息队列通知机制,通知其他 Java 应用程序进行刷新。

这里简单介绍一下爱奇艺缓存发展的五个阶段,当然还有一些其他的优化,比如 GC 调优,缓存穿透,缓存覆盖的一些优化等等。

原始社会 - 查库

上面说的是爱奇艺的一个进化线路,但是在大家的一般开发过程中,第一步一般都没有 Redis,而是直接查库。

在流量不大的时候,查数据库或者读取文件最为方便,也能完全满足我们的业务要求。

古代社会 - HashMap

当我们应用有一定流量之后或者查询数据库特别频繁,这个时候就可以祭出我们 Java 中自带的 HashMap 或者 ConcurrentHashMap。我们可以在代码中这么写:

public class CustomerService { private HashMap < String ,
String

hashMap = new HashMap <>(); private CustomerMapper customerMapper; public String getCustomer( String name){
String customer = hashMap. get (name); if ( customer == null ){
customer = customerMapper. get (name); hashMap.put(name,customer); }
return customer; } }

但是这样做就有个问题 HashMap 无法进行数据淘汰,内存会无限制的增长,所以 HashMap 很快也被淘汰了。

当然并不是说它完全就没用,就像我们古代社会也不是所有的东西都是过时的,比如我们中华名族的传统美德是永不过时的,就像这个 HashMap 一样的可以在某些场景下作为缓存,当不需要淘汰机制的时候,比如我们利用反射,如果我们每次都通过反射去搜索 Method,field,性能必定低效,这时我们用 HashMap 将其缓存起来,性能能提升很多。

近代社会 - LRUHashMap

在古代社会中难住我们的问题是无法进行数据淘汰,这样会导致我们内存无限膨胀,显然我们是不可以接受的。

有人就说我把一些数据给淘汰掉呗,这样不就对了,但是怎么淘汰呢?随机淘汰吗?当然不行,试想一下你刚把 A 装载进缓存,下一次要访问的时候就被淘汰了,那又会访问我们的数据库了,那我们要缓存干嘛呢?

所以聪明的人们就发明了几种淘汰算法,下面列举下常见的三种 FIFO,LRU,LFU(还有一些 ARC,MRU 感兴趣的可以自行搜索):

FIFO:先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰。

这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。

LRU:最近最少使用算法。
在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。

但是这个依然有个问题,如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),再后 1 分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。

LFU:最近最少频率使用。
在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。

上面列举了三种淘汰策略,对于这三种,实现成本是一个比一个高,同样的命中率也是一个比一个好。

而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的 LRU,如何实现一个 LRUMap 呢?我们可以通过继承 LinkedHashMap,重写 removeEldestEntry 方法,即可完成一个简单的 LRUMap。

class LRUMap extends LinkedHashMap { private final int
max; private Object lock ; public LRUMap ( int max, Object
lock ) { //无需扩容 super (( int ) (max *
1.4f ),
0.75f , true ); this .max = max; this . lock = lock ; } /** * 重写LinkedHashMap的removeEldestEntry方法即可 * 在Put的时候判断,如果为true,就会删除最老的 * @param eldest * @return */ @Override protected boolean removeEldestEntry( Map . Entry eldest) { return size() > max; }
public Object getValue( Object key) { synchronized ( lock ) {
return get (key); } } public void putValue( Object key,
Object value) { synchronized ( lock ) { put(key, value); } }
public boolean removeValue( Object key) { synchronized ( lock )
{ return remove(key) != null ; } } public boolean
removeAll(){ clear(); return true ; } }

在 LinkedHashMap 中维护了一个 entry(用来放 key 和 value 的对象)链表。在每一次 get 或者 put 的时候都会把插入的新 entry,或查询到的老 entry 放在我们链表末尾。

可以注意到我们在构造方法中,设置的大小特意设置到 max*1.4,在下面的 removeEldestEntry 方法中只需要 size>max 就淘汰,这样我们这个 map 永远也走不到扩容的逻辑了,通过重写 LinkedHashMap,几个简单的方法我们实现了我们的 LruMap。

现代社会 - Guava Cache

在近代社会中已经发明出来了 LRUMap,用来进行缓存数据的淘汰,但是有几个问题:

锁竞争严重,可以看见我的代码中,Lock 是全局锁,在方法级别上面的,当调用量较大时,性能必然会比较低。
不支持过期时间
不支持自动刷新
所以谷歌的大佬们对于这些问题,按捺不住了,发明了 Guava Cache,在 Guava Cache 中你可以如下面的代码一样,轻松使用:

public static void main( String [] args) throws
ExecutionException { LoadingCache < String , String

cache = CacheBuilder .newBuilder() .maximumSize( 100 ) //写之后30ms过期 .expireAfterWrite( 30L , TimeUnit .MILLISECONDS)
//访问之后30ms过期 .expireAfterAccess( 30L , TimeUnit .MILLISECONDS)
//20ms之后刷新 .refreshAfterWrite( 20L , TimeUnit .MILLISECONDS)
//开启weakKey key 当启动垃圾回收时,该缓存也被回收 .weakKeys()
.build(createCacheLoader()); System . out .println(cache. get (
“hello” )); cache.put( “hello1” , “我是hello1” ); System . out
.println(cache. get ( “hello1” )); cache.put( “hello1” , “我是hello2”
); System . out .println(cache. get ( “hello1” )); } public
static com.google.common.cache. CacheLoader < String , String
createCacheLoader() { return new com.google.common.cache. CacheLoader < String , String
() { @Override public String load( String key) throws Exception { return key; } }; }

我将会从 Guava Cache 原理中,解释 Guava
Cache 是如何解决 LRUMap 的几个问题的。

锁竞争

Guava Cache 采用了类似 ConcurrentHashMap 的思想,分段加锁,在每个段里面各自负责自己的淘汰的事情。

在 Guava 根据一定的算法进行分段,这里要说明的是,如果段太少那竞争依然很严重,如果段太多容易出现随机淘汰,比如大小为 100 的,给他分 100 个段,那也就是让每个数据都独占一个段,而每个段会自己处理淘汰的过程,所以会出现随机淘汰。在 Guava Cache 中通过如下代码,计算出应该如何分段。

int segmentShift = 0 ; int segmentCount = 1 ; while
(segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount *
20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1 ; }

上面 segmentCount 就是我们最后的分段数,其保证了每个段至少 10 个 entry。如果没有设置 concurrencyLevel 这个参数,那么默认就会是 4,最后分段数也最多为 4,例如我们 size 为 100,会分为 4 段,每段最大的 size 是 25。

在 Guava Cache 中对于写操作直接加锁,对于读操作,如果读取的数据没有过期,且已经加载就绪,不需要进行加锁,如果没有读到会再次加锁进行二次读,如果还没有需要进行缓存加载,也就是通过我们配置的 CacheLoader,我这里配置的是直接返回 Key,在业务中通常配置从数据库中查询。 如下图所示:

在这里插入图片描述

过期时间

相比于 LRUMap 多了两种过期时间,一个是写后多久过期 expireAfterWrite,一个是读后多久过期 expireAfterAccess。

很有意思的事情是,在 Guava Cache 中对于过期的 entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。看下面的代码:

public static void main( String [] args) throws
ExecutionException , InterruptedException { Cache < String ,
String

cache = CacheBuilder .newBuilder() .maximumSize( 100 ) //写之后5s过期 .expireAfterWrite( 5 , TimeUnit .MILLISECONDS) .concurrencyLevel( 1
) .build(); cache.put( “hello1” , “我是hello1” ); cache.put(
“hello2” , “我是hello2” ); cache.put( “hello3” , “我是hello3” );
cache.put( “hello4” , “我是hello4” ); //至少睡眠5ms Thread .sleep( 5 );
System . out .println(cache.size()); cache.put( “hello5” ,
“我是hello5” ); System . out .println(cache.size()); } 输出: 4 1

从这个结果中我们知道,在 put 的时候才进行的过期处理。特别注意的是我上面 concurrencyLevel(1)这里将分段最大设置为 1,不然不会出现这个实验效果的,在上面一节中已经说过,我们是以段位单位进行过期处理。在每个 Segment 中维护了两个队列:

final Queue < ReferenceEntry <K, V>> writeQueue; final Queue <
ReferenceEntry <K, V>> accessQueue;

writeQueue 维护了写队列,队头代表着写得早的数据,队尾代表写得晚的数据。accessQueue 维护了访问队列,和 LRU 一样,用来进行访问时间的淘汰。如果当这个 Segment 超过最大容量,比如我们上面所说的 25,超过之后,就会把 accessQueue 这个队列的第一个元素进行淘汰。

void expireEntries( long now) { drainRecencyQueue();
ReferenceEntry <K, V> e; while ((e = writeQueue.peek()) != null
&& map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(),
RemovalCause .EXPIRED)) { throw new AssertionError (); } }
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause .EXPIRED)) { throw
new AssertionError (); } } }

上面就是 Guava Cache 处理过期 entries 的过程,会对两个队列一次进行 peek 操作,如果过期就进行删除。

一般处理过期 entries 可以在我们的 put 操作的前后,或者读取数据时发现过期了,然后进行整个 segment 的过期处理,又或者进行二次读 lockedGetOrLoad 操作的时候调用。

void evictEntries( ReferenceEntry <K, V> newest) { ///… 省略无用代码
while (totalWeight > maxSegmentWeight) { ReferenceEntry <K, V> e =
getNextEvictable(); if (!removeEntry(e, e.getHash(), RemovalCause
.SIZE)) { throw new AssertionError (); } } } /**
**返回accessQueue的entry
**/ ReferenceEntry <K, V> getNextEvictable() { for ( ReferenceEntry <K, V> e : accessQueue) { int weight =
e.getValueReference().getWeight(); if (weight > 0 ) { return e;
} } throw new AssertionError (); }

上面是我们驱逐 entry 的时候的代码,可以看见访问的是 accessQueue 对其队头进行驱逐。而驱逐策略一般是在对 segment 中的元素发生变化时进行调用,比如插入操作,更新操作,加载数据操作。

自动刷新

自动刷新操作,在 Guava Cache 中实现相对比较简单,直接通过查询,判断其是否满足刷新条件,进行刷新。

其他特性

在 Guava Cache 中还有一些其他特性:

虚引用

在 Guava Cache 中,key 和 value 都能进行虚引用的设定,在 segment 中有两个引用队列:

final @Nullable ReferenceQueue keyReferenceQueue; final
@Nullable ReferenceQueue valueReferenceQueue;

这两个队列用来记录被回收的引用,其中每个队列记录了每个被回收的 entry 的 hash,这样回收了之后通过这个队列中的 hash 值就能把以前的 entry 进行删除。

删除监听器

在 Guava Cache 中,当有数据被淘汰时,但是你不知道他到底是过期,还是被驱逐,还是因为虚引用的对象被回收?

这个时候你可以调用这个方法 removalListener(RemovalListener listener)添加监听器进行数据淘汰的监听,可以打日志或者一些其他处理,可以用来进行数据淘汰分析。

在 RemovalCause 记录了所有被淘汰的原因:被用户删除,被用户替代,过期,驱逐收集,由于大小淘汰。

Guava Cache 的总结

细细品读 Guava Cache 的源码总结下来,其实就是一个性能不错的,api 丰富的 LRU Map。爱奇艺的缓存的发展也是基于此之上,通过对 Guava Cache 的二次开发,让其可以进行 Java 应用服务之间的缓存更新。

走向未来-caffeine

Guava Cache 的功能的确是很强大,满足了绝大多数人的需求,但是其本质上还是 LRU 的一层封装,所以在众多其他较为优良的淘汰算法中就相形见绌了。而 Caffeine Cache 实现了 W-TinyLFU(LFU+LRU 算法的变种)。下面是不同算法的命中率的比较:
在这里插入图片描述

其中 Optimal 是最理想的命中率,LRU 和其他算法相比的确是个弟弟。而我们的 W-TinyLFU 是最接近理想命中率的。当然不仅仅是命中率 Caffeine 优于了 Guava Cache,在读写吞吐量上面也是完爆 Guava Cache。
在这里插入图片描述

这个时候你肯定会好奇为啥 Caffeine 这么牛逼呢?别着急下面慢慢给你道来。

W-TinyLFU

上面已经说过了传统的 LFU 是怎么一回事。在 LFU 中只要数据访问模式的概率分布随时间保持不变时,其命中率就能变得非常高。

这里我还是拿爱奇艺举例,比如有部新剧出来了,我们使用 LFU 给他缓存下来,这部新剧在这几天大概访问了几亿次,这个访问频率也在我们的 LFU 中记录了几亿次。

但是新剧总会过气的,比如一个月之后这个新剧的前几集其实已经过气了,但是他的访问量的确是太高了,其他的电视剧根本无法淘汰这个新剧,所以在这种模式下是有局限性。

所以各种 LFU 的变种出现了,基于时间周期进行衰减,或者在最近某个时间段内的频率。同样的 LFU 也会使用额外空间记录每一个数据访问的频率,即使数据没有在缓存中也需要记录,所以需要维护的额外空间很大。

可以试想我们对这个维护空间建立一个 HashMap,每个数据项都会存在这个 HashMap 中,当数据量特别大的时候,这个 HashMap 也会特别大。
再回到 LRU,我们的 LRU 也不是那么一无是处,LRU 可以很好的应对突发流量的情况,因为他不需要累计数据频率。

所以 W-TinyLFU 结合了 LRU 和 LFU,以及其他的算法的一些特点。

频率记录

首先要说到的就是频率记录的问题,我们要实现的目标是利用有限的空间可以记录随时间变化的访问频率。在 W-TinyLFU 中使用 Count-Min Sketch 记录我们的访问频率,而这个也是布隆过滤器的一种变种。如下图所示::
在这里插入图片描述

如果需要记录一个值,那我们需要通过多种 hash 算法对其进行处理hash,然后在对应的 hash 算法的记录中+1,为什么需要多种 hash 算法呢?

由于这是一个压缩算法必定会出现冲突,比如我们建立一个 Long 的数组,通过计算出每个数据的 hash 的位置。比如张三和李四,他们俩有可能 hash 值都是相同,比如都是 1 那 Long[1] 这个位置就会增加相应的频率,张三访问 1 万次,李四访问 1 次那 Long[1] 这个位置就是 1 万零 1。

如果取李四的访问评率的时候就会取出是 1 万零 1,但是李四命名只访问了 1 次啊,为了解决这个问题,所以用了多个 hash 算法可以理解为 Long[][] 二维数组的一个概念,比如在第一个算法张三和李四冲突了,但是在第二个,第三个中很大的概率不冲突,比如一个算法大概有 1% 的概率冲突,那四个算法一起冲突的概率是 1% 的四次方。

通过这个模式,我们取李四的访问率的时候取所有算法中,李四访问最低频率的次数。所以他的名字叫 Count-Min Sketch。
在这里插入图片描述

这里和以前的做个对比,简单的举个例子:如果一个 HashMap 来记录这个频率,如果我有 100 个数据,那这个 HashMap 就得存储 100 个这个数据的访问频率。

哪怕我这个缓存的容量是 1,因为 LFU 的规则我必须全部记录这 100 个数据的访问频率。如果有更多的数据我就有记录更多的。

在 Count-Min Sketch 中,我这里直接说 Caffeine 中的实现吧(在 FrequencySketch 这个类中),如果你的缓存大小是 100,他会生成一个 Long 数组大小是和 100 最接近的 2 的幂的数,也就是 128。

而这个数组将会记录我们的访问频率。在 Caffeine 中它规则频率最大为 15,15 的二进制位 1111,总共是 4 位,而 Long 型是 64 位。所以每个 Long 型可以放 16 种算法,但是 Caffeine 并没有这么做,只用了四种 hash 算法,每个 Long 型被分为四段,每段里面保存的是四个算法的频率。

这样做的好处是可以进一步减少 hash 冲突,原先 128 大小的 hash,就变成了 128X4。

一个Long的结构如下:
在这里插入图片描述

我们的 4 个段分为 A,B,C,D,在后面我也会这么叫它们。而每个段里面的四个算法我叫他 s1,s2,s3,s4。下面举个例子,如果要添加一个访问 50 的数字频率应该怎么做?我们这里用 size=100 来举例。

首先确定 50 这个 hash 是在哪个段里面,通过 hash & 3 必定能获得小于 4 的数字,假设 hash & 3=0,那就在 A 段。
对 50 的 hash 再用其他 hash 算法再做一次 hash,得到 Long 数组的位置。假设用 s1 算法得到 1,s2 算法得到 3,s3 算法得到 4,s4 算法得到 0。
然后在 Long[1] 的 A 段里面的 s1 位置进行+1,简称 1As1 加 1,然后在 3As2 加 1,在 4As3 加 1,在 0As4 加 1。
在这里插入图片描述

这个时候有人会质疑频率最大为 15 的这个是否太小?没关系在这个算法中,比如 size 等于 100,如果他全局提升了 1000 次就会全局除以 2 衰减,衰减之后也可以继续增加,这个算法再 W-TinyLFU 的论文中证明了其可以较好的适应时间段的访问频率。

读写性能

在 Guava Cache 中我们说过其读写操作中夹杂着过期时间的处理,也就是你在一次 put 操作中有可能还会做淘汰操作,所以其读写性能会受到一定影响。

可以看上面的图中,Caffeine 的确在读写操作上面完爆 Guava Cache。主要是因为在 Caffeine,对这些事件的操作是通过异步操作,它将事件提交至队列,这里的队列的数据结构是 RingBuffer。

然后会通过默认的 ForkJoinPool.commonPool(),或者自己配置线程池,进行取队列操作,然后在进行后续的淘汰,过期操作。

当然读写也是有不同的队列,在 Caffeine 中认为缓存读比写多很多,所以对于写操作是所有线程共享一个 Ringbuffer。
在这里插入图片描述

对于读操作比写操作更加频繁,进一步减少竞争,其为每个线程配备了一个 RingBuffer:
在这里插入图片描述

数据淘汰策略

在 Caffeine 所有的数据都在 ConcurrentHashMap 中,这个和 Guava Cache 不同,Guava Cache 是自己实现了个类似 ConcurrentHashMap 的结构。在 Caffeine 中有三个记录引用的 LRU 队列:

Eden 队列:在 Caffeine 中规定只能为缓存容量的 %1,如果 size=100,那这个队列的有效大小就等于 1。这个队列中记录的是新到的数据,防止突发流量由于之前没有访问频率,而导致被淘汰。
比如有一部新剧上线,在最开始其实是没有访问频率的,防止上线之后被其他缓存淘汰出去,而加入这个区域。伊甸区,最舒服最安逸的区域,在这里很难被其他数据淘汰。

Probation 队列:叫做缓刑队列,在这个队列就代表你的数据相对比较冷,马上就要被淘汰了。这个有效大小为 size 减去 eden 减去 protected。
Protected 队列:在这个队列中,可以稍微放心一下了,你暂时不会被淘汰,但是别急,如果 Probation 队列没有数据了或者 Protected 数据满了,你也将会面临淘汰的尴尬局面。
当然想要变成这个队列,需要把 Probation 访问一次之后,就会提升为 Protected 队列。这个有效大小为(size 减去 eden) X 80% 如果 size =100,就会是 79。

这三个队列关系如下:
在这里插入图片描述

所有的新数据都会进入 Eden。
Eden 满了,淘汰进入 Probation。
如果在 Probation 中访问了其中某个数据,则这个数据升级为 Protected。
如果 Protected 满了又会继续降级为 Probation。
对于发生数据淘汰的时候,会从 Probation 中进行淘汰,会把这个队列中的数据队头称为受害者,这个队头肯定是最早进入的,按照 LRU 队列的算法的话那它就应该被淘汰,但是在这里只能叫它受害者,这个队列是缓刑队列,代表马上要给它行刑了。

这里会取出队尾叫候选者,也叫攻击者。这里受害者会和攻击者做 PK,通过我们的 Count-Min Sketch 中的记录的频率数据有以下几个判断:

如果攻击者大于受害者,那么受害者就直接被淘汰。
如果攻击者<=5,那么直接淘汰攻击者。这个逻辑在他的注释中有解释: 他认为设置一个预热的门槛会让整体命中率更高。
在这里插入图片描述

其他情况,随机淘汰。
如何使用

对于熟悉 Guava 的玩家来说,如果担心有切换成本,那么你就多虑了, Caffeine 的 api 借鉴了 Guava 的 api,可以发现其基本一模一样。

public static void main( String [] args) { Cache < String ,
String

cache = Caffeine .newBuilder() .expireAfterWrite( 1 , TimeUnit .SECONDS) .expireAfterAccess( 1 , TimeUnit .SECONDS) .maximumSize(
10 ) .build(); cache.put( “hello” , “hello” ); }

顺便一提的是,越来越多的开源框架都放弃了 Guava Cache,比如 Spring5。在业务上我也曾经比较过 Guava Cache 和 Caffeine,最终选择了 Caffeine,在线上也有不错的效果。所以不用担心 Caffeine 不成熟,没人使用。

最后

本文主要讲述爱奇艺的缓存之路和本地缓存的一个发展历史(从古至今到未来),以及每一种缓存的实现基本原理。

喜欢小编轻轻点个关注吧!

一、C++反作弊对抗实战 (基础篇 —— 2.注册表注入DLL) 在Windows NT/XP操作系统中,当需要加载user32.dll的程序启动时,user32.dll会加载注册表键HLM\Software\Microsoft\WindowsNT\CurrentVersion\Windows\AppInit_dlls下面列出的所有模块。 阅读详情

相关推荐

硬件基础(4):(7)ADC电压采集:软件校准(标定、拟合)

在实际项目中,**软件端校准**可以有效弥补硬件方面(分压电阻误差、放大器失调、参考源误差等)带来的系统性偏差,从而提高 ADC 测量精度。下面是一个常见的**多点实测校准**流程与思路,个人感觉传感器标定的原理也只这样。

FCZlll的博客 6246

你应该知道的缓存进化史

1. 背景本文是上周去技术沙龙听了一下爱奇艺的Java缓存之路有感写出来的。先简单介绍一下爱奇艺的java缓存道路的发展吧。可以看见图中分为几个阶段:第一阶段:数据同步加...

架构文摘 1438

模拟波束赋形 vs 数字波束赋形:5G天线设计中的成本与性能权衡

本文深入剖析了5G天线设计中模拟波束赋形与数字波束赋形的核心差异,重点探讨了二者在硬件成本、功耗、网络覆盖能力和频谱效率等方面的权衡。文章指出,模拟方案成本低但灵活性有限,数字方案性能强大但代价高昂,而混合波束赋形已成为主流折中方案。决策需紧密结合具体的部署场景、性能要求和总拥有成本进行综合考量。

weixin_28432777的博客 840

java游戏原始部落_你应该知道的Java缓存进化史

背景本文是上周去技术沙龙听了一下爱奇艺的 Java 缓存之路有感写出来的。先简单介绍一下爱奇艺的 Java 缓存道路的发展吧。可以看见图中分为几个阶段:第一阶段:数据同步加 Redis通过消息队列进行数据同步至 Redis,然后 Java 应用直接去取缓存。这个阶段的优点是:由于是使用的分布式缓存,所以数据更新快。缺点也比较明显:依赖 Redis 的稳定性,一旦 Redis 挂了,整个缓存系统不可...

weixin_39759182的博客 359

机器学习初探——用线性回归预测股价趋势

在进入代码之前,先建立一个大框架。类型一句话解释金融中的应用监督学习给机器“标准答案”让它学预测股价(历史数据→未来价格)无监督学习不给答案,让机器自己找规律把相似股票自动归类、发现异常交易强化学习让机器通过“试错”学习最优策略AI自动交易、量化策略优化我们现阶段只学“监督学习”——这是最常用、最容易上手的。准备一堆“特征”和对应的“标签”(比如:历史的第N天 → 那天的价格)喂给机器一个“算法”(比如线性回归)机器“拟合”出一个规律(比如找到一条最合适的直线)

110

7.26华为OD机试真题 新系统 - 炸弹人的雷区数量 (Java/Py/C/C++/Js/Go)

7.26华为OD机试真题 新系统 - 炸弹人的雷区数量 (Java/Py/C/C++/Js/Go)

算法大师! 794

Java缓存进化史

背景   本文是上周去技术沙龙听了一下爱奇艺的 Java 缓存之路有感写出来的。先简单介绍一下爱奇艺的 Java 缓存道路的发展吧。 可以看见图中分为几个阶段:   第一阶段:数据同步加 Redis   通过消息队列进行数据同步至 Redis,然后 Java 应用直接去取缓存。这个阶段的优点是:由于是使用的分布式缓存,所以数据更新快。缺点也比较明显:依赖 Redis 的稳定...

DPnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnice的博客 383

【异常】XXL-job触发调度:address:http://192.168.x.x:x/ code:500 msg:xxl-rpc remoting error(connect timed out)

XXL-JOB调度任务失败分析:调度中心(10.254.234.80)与执行器(192.168.x.x)连接超时,返回500错误。主要原因是网络不通或执行器异常:1.网络不通可能由防火墙或网段隔离导致;2.执行器未启动或端口未监听;3.地址配置错误。解决方案:1.关闭代理使用内网;2.测试网络连通性和端口可用性;3.检查执行器进程状态和端口监听情况;4.核对执行器地址配置;5.必要时配置防火墙规则放行端口。

奶爸的编程之路,也就一周冷个三天 414

爱奇艺的Java缓存之路,你应该知道的缓存进化史!

本文是上周去技术沙龙听了一下爱奇艺的Java缓存之路有感写出来的。先简单介绍一下爱奇艺的java缓存道路的发展吧。可以看见图中分为几个阶段:第一阶段:数据同步加redis...

大数据星球-浪尖 312

BoT-SORT实战:手把手教你实现BoT-SORT训练和测试

文章目录摘要1、数据准备2、配置BoT-SORT环境2.1、获取代码2.2、创建虚拟环境2.3、安装必要的包2.3.1、安装pytorch2.3.2、安装cython2.3.3、安装Cython-bbox2.3.4、安装faiss2.3.5、安装其他的包3、训练3.1、生成REID数据集3.2、预训练模型3.2.1、ByteTrack预训练模型3.2.2、REID预训练模型3.3、开始训练4、如何更改FASTREID的BackBone?5、测试5.1、测试视频5.2、测试摄像头5.3、多类别测试 摘要 Bo

m0_47867638的博客 2298

【以太网硬件十九】SGMII到底能不能直接出光模块?

详细解释为什么SGMII可以直接出光模块及其使用限制

highman110的博客 1万+

你应该知道的缓存进化史!

1、背景本文是上周去技术沙龙听了一下爱奇艺的Java缓存之路有感写出来的。先简单介绍一下爱奇艺的java缓存道路的发展吧。可以看见图中分为几个阶段:第一阶段:数据同步加r...

Java后端技术 1455

【反卷积算法】Richardson-Lucy (RL) :通过不断迭代更新图像估计值

Richardson-Lucy(RL)反卷积算法:是一种基于最大似然估计(Maximum Likelihood Estimation, MLE)的图像去卷积方法,常用于去模糊和图像增强。通过不断地更新图像估计值,来最小化模糊图像和恢复图像之间的误差,逐步消除模糊,最终接近真实的原始图像,每次迭代都会根据当前估计的图像来修正下次迭代的结果。

📷 图像处理践行者 | 传统算法 · 深度学习 · 多模态 | 分享实战经验与技术洞见,探索从像素到智能的无限可能。 3042

数据缓存刷新_你应该知道的缓存进化史

作者:咖啡拿铁来源:https://www.jianshu.com/p/98a387bd92d81、背景本文是上周去技术沙龙听了一下爱奇艺的Java缓存之路有感写出来的。先简单介绍一下爱奇艺的java缓存道路的发展吧。可以看见图中分为几个阶段:第一阶段:数据同步加redis通过消息队列进行数据同步至redis,然后Java应用直接去取缓存 这个阶段优点是:由于是使用的分布式缓存,所以数据更新快。缺...

weixin_39781143的博客 309

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

最近看到的缓存好文章(1):你应该知道的缓存进化史

1.背景 本文是上周去技术沙龙听了一下爱奇艺的Java缓存之路有感写出来的。先简单介绍一下爱奇艺的java缓存道路的发展吧。 可以看见图中分为几个阶段: 第一阶段:数据同步加redis 通过消息队列进行数据同步至redis,然后Java应用直接去取缓存 这个阶段优点是:由于是使用的分布式缓存,所以数据更新快。缺点也比较明显:依赖Redis的稳定性,一旦redis挂了,整个缓存系统不可用,造成缓存雪崩,所有请求打到DB。 第二,三阶段:JavaMap到Guava cache 这个...

.zz的博客 283

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

RP2040 MicroPython DMA内存到内存传输实战教程

DMA(直接内存访问)是嵌入式系统高效搬移数据的关键机制,它能让数据在内存区域间自动传输而无需CPU逐字节干预。在MicroPython这类解释型环境中,许多开发者认为DMA遥不可及,但RP2040官方固件早已开放DMA支持。理解DMA的触发源、传输计数和地址递增原理,是优化数据搬运性能的基础。在帧缓冲滚动、音频波形合成、传感器数据采集等场景中,利用DMA可显著提升CPU利用率。本文聚焦RP2040上的MicroPython环境,从内存到内存传输这一最基础模式出发,通过实例演示如何用Python代码配置DM

weixin_34060299的博客 397

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

JGrowing缓存技术演进:从基础缓存到Caffeine的未来缓存

在Java应用开发中,缓存技术是提升系统性能的关键手段。JGrowing项目通过多年的实践积累,总结出了一套完整的缓存技术演进路线,从最基础的HashMap到现代的Caffeine缓存,每一个阶段都对应着不同的技术需求和解决方案。 ## 缓存技术演进的历史脉络 ### 原始阶段:直接查询数据库 在系统初期,流量不大的情况下,直接查询数据库或读取文件是最简单直接的解决方案。这个阶段完全不需要考虑

gitblog_01152的博客 686

ehcache 手动刷新缓存_缓存的那些事

​作者 | 黄永灿后端开发攻城狮,关注服务端技术与性能优化。什么时候需要缓存一般来说会从两个方面来判断是否需要缓存:CPU 占用:如果某些应用需要消耗大量的 CPU 资源去计算,例如你使用正则表达式比较频繁或者业务查询链很复杂时,而它又占用了很多 CPU 的话,那就应该使用缓存将结果缓存下来。数据库 IO 占用:如果你发现你的数据库连接池比较空闲,可以不用缓存。但是如果数据库连接池比较繁忙,甚至经...

weixin_42361708的博客 1172

Java分布式缓存这匹“野马”,你驾驭得了吗?

俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将分为如下几个方面介绍如何利用好缓存: 你真的需要缓存吗 如何选择合适的缓存 多级缓存 缓存更新 缓存挖坑三剑客 缓存污染 序列化 GC调优 缓存的监控 一款好的框架 总结 你真的需要缓存吗 在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入一定的技术复杂度,一般来说从两个方面来判...

qq_41534566的博客 1785

【Android】如何优雅的设计和使用缓存

如何优雅的设计和使用缓存? 背景 在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介绍如何利用好缓存。 1.确认是否需要缓存 在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入的一定的技术复杂度,后文也将会一一介绍这些复杂度。一般来说从两个方面来个是否需要使用缓存: ...

远方不远 913
上一篇: 我是一个Java class
下一篇: 一文精通kafka 消费者的三种语义
java1856905
博客等级 码龄8年 100粉丝 249原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值