本地高性能缓存Caffeine

一、Caffeine介绍

        Caffeine是一个基于Java 8的高性能本地缓存框架,采用了W-TinyLFU(LUR和LFU的优点结合)算法,实现了缓存高命中率、内存低消耗。缓存性能接近理论最优,属于是Guava Cache的增强版。在并发读、并发写、并发读写三个场景下Caffeine的性能最优。

二、使用方式

1、在pom.xml 中添加 caffeine 依赖

<dependency>    
    <groupId>com.github.ben-manes.caffeine</groupId>      
    <artifactId>caffeine</artifactId>  
</dependency>

2、参数说明

参数

说明

expireAfterWrite

写缓存后n秒钟过期

expireAfterAccess

读写缓存后n秒钟过期

refreshAfterWrite

写入后多久刷新,刷新是基于访问(支持异步和同步刷新)。

只阻塞当前数据加载的线程,其他线程返回旧值,刷新完成才会返回新的取值。

expireAfter

自定义淘汰策略, Caffeine 通过时间轮算法来实现不同key 的不同过期时间

maximumSize

缓存key最大个数

weakKeys

key设置为弱引用,在 GC 时可以直接淘汰

weakValues

value设置为弱引用,在 GC 时可以直接淘汰

softValues

value设置为软引用,在内存溢出前可以直接淘汰

executor

选择自定义线程池,默认线程池是 ForkJoinPool.commonPool()

maximumWeight

设置缓存最大权重

weigher

设置具体key权重

recordStats

缓存的统计数据,比如命中率等

removalListener

缓存淘汰监听器

writer

缓存写入、更新、淘汰的监听器

3、创建对象

private LoadingCache<String, List<Integer>> cache = Caffeine.newBuilder()
        //cache最大缓存数
        .maximumSize(500)
        //设置写缓存后n秒钟过期
        .expireAfterWrite(17, TimeUnit.SECONDS)
        .build();

4、缓存类型

4.1 手动加载cache:提供搜索查找、更新和移除缓存元素。缓存数据不存在,需要手动加载。

Cache<String, List<Integer>> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(500)
        .build(new CacheLoader<String, List<Integer>>() {
            @Override
            public List<Integer> load(String key){
                return createCache(key);
            }
        });
private static final String KEY = "key";
private void getCache() {
    //1.查询缓存,没有返回null
    List<Integer> list = cache.getIfPresent(KEY);
    //2.查找缓存,不存在生成缓存,生成失败返回null
    list = cache.get(KEY, k -> createCache(KEY));
    //3.添加或者更新一个缓存元素
    cache.put(KEY, list);
    //4.移除一个缓存元素
    cache.invalidate(KEY);
}

4.2 自动加载Loading Cache:LoadingCache比Cache多了如果缓存不存在,会通过CacheLoader.load生成对应的缓存数据到缓存中,实现自动加载。

LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .maximumSize(500)
        .build(key -> createCache(key));

private void getLoadingCache() {
    //1.查找缓存,不存在生成缓存,生成失败返回null
    List<Integer> list = loadingCache.get(KEY);
    //2.批量查找缓存,如果缓存不存在则生成缓存
    List<String> requestKeys = new ArrayList<>();
    Map<String, List<Integer>> map = loadingCache.getAll(requestKeys);
}

4.3 手动异步加载Async Cache:AsyncCache是Cache异步形式,提供了Executor生成缓存并返回CompletableFuture的能力。默认的线程池实现是 ForkJoinPool.commonPool() 。也可以使用Caffeine.executo设置自定义的线程池。

AsyncCache<String, List<Integer>> asyncCache = Caffeine.newBuilder()
        .maximumSize(500)
        .buildAsync();
 
private void getAsyncCache() {
    //1.查询缓存,没有返回null
    CompletableFuture<List<Integer>> graph = asyncCache.getIfPresent(KEY);
    //2.查找缓存,如果不存在,异步生成
    graph = asyncCache.get(KEY, k -> createCache(KEY));
    //3.添加或者更新一个缓存元素
    asyncCache.put(KEY, graph);
    //4.移除一个缓存元素
    asyncCache.synchronous().invalidate(KEY);
}

4.4 自动异步加载Async Loading Cache:AsyncLoadingCache是LoadingCache的异步形式,返回一个包含取值的CompletableFuture。

AsyncLoadingCache<String, List<Integer>> asyncLoadingCache = Caffeine.newBuilder()
            .maximumSize(500)
            .buildAsync(key->{
              log.info("缓存不存在异步加载");
              return createCache(key);
            });
//查找不存在异步生成
CompletableFuture<Graph> graph = cache.get(key);
//批量查找不存在异步生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

5、驱逐策略

       Caffeine提供三类驱逐策略:基于大小、基于时间、基于引用。

5.1 基于大小

//1:个数(大小)
LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .maximumSize(500)
        .build();
//2.基于缓存内元素权重
LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .maximumWeight(200)
        .weigher((String key, List<Integer> list) -> list.size())
        .build();

备注:maximumWeight 与 maximumSize 不可以同时使用。

5.2 基于时间

//1.设置写缓存后n秒钟过期      
LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .build();       
 
//2.设置读写缓存后n秒钟过期,类似于Guava Cache的expireAfterWrite    
LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .expireAfterAccess(10, TimeUnit.SECONDS)
        .build();
 
//3.自定义策略需要自己实现Expiry接口。
LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .expireAfter(new Expiry<String, String>() {
                    //创建多久后过期
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        return currentTime;
                    }
                    //更新多久后过期
                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentTime;
                    }
                    //读多久后过期
                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentTime;
                    }
                })
        .build();

5.3 基于引用

//1.当key和缓存都不存在其他强引用的时候
 LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .weakKeys()    
        .weakValues()
        .build();   
//2.进行GC的时候
 LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .softValues()
        .build(); 

6、刷新机制

 LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
         //只阻塞当前数据加载的线程,其他线程返回旧值。刷新完毕才会返回新的取值
        .refreshAfterWrite(30, TimeUnit.SECONDS)
        .build();

说明:刷新机制依赖于读操作,比如设置1天过期,到期没有访问也不会自动刷新,get方法内 源码afterRead()方法后的refreshIfNeeded()方法根据同步或异步刷新。

7、统计功能

Caffeine.recordStats()可以打开数据收集功能。使用Cache.stats()方法将会返回一个CacheStats对象,包含一些统计指标,如:

  • hitRate(): 查询缓存的命中率
  • evictionCount(): 被驱逐的缓存数量
  • averageLoadPenalty(): 新值被载入的平均耗时
LoadingCache<String, List<Integer>> loadingCache = Caffeine.newBuilder()
        .recordStats()
        .build(key -> createCache(key));

三、淘汰算法

1、FIFO(先进先出)算法

        FIFO是类似队列的算法,如果空间满了需要加入新数据先进入缓存的数据会被优先被淘汰。

淘汰过程:

优点:最简单,最公平的一种数据淘汰算法,逻辑简单,易于实现。

缺点:缓存命中率低。

2、LRU(最近最久未使用)算法

        如果空间用尽需要加入新数据会优先淘汰最久未被访问(访问少的在未来被访问的概率也是最低的)的数据。LRU是以时间先后顺序来衡量要淘汰的数据。适用于局部突发流量场景。

淘汰过程:

优点:

  1. 实现简单,一般情况下会有很好的命中率,比较常用。
  2. 有效的对访问比较频繁的数据进行保护,提高热点数据的命中率。
  3. 局部突发流量场景,对突发性的稀疏流量(n个1,n个2,突然来了1个3,3就属于突然稀疏流量)表现很好。

缺点: 在存在周期性(比如12345,12345的访问)的局部热点数据场景,空间满了会导致周期性的局部热点数据被淘汰。不能很好的处理偶发性,周期性的数据,影响命中率。

3、LFU(最近最少使用)算法

        如果空间用尽需要加入新数据会优先淘汰时间段内访问次数最低(访问次数少的在未来被使用的概率也很小)的数据。LFU是以时间段内的使用次数衡量淘汰的数据。适用于局部周期性流量场景。

淘汰过程:插入数据满了,优先淘汰在一定时间间隔内的访问频率最低的元素。

优点:

  1. LFU适用于局部周期性的场景,有很好的命中率。
  2. 在局部周期性流量场景下,LFU是以次数为基准,能准确的保证和提高命中率。

缺点:

  1. 需要记录数据的访问频次,需要额外的空间开销。
  2. 需要给每个记录项维护频率信息,每次访问都需要更新,增加时间开销。
  3. 对突发性的稀疏流量场景下,算法命中率会下降。原因:LFU针对访问次数的大小淘汰,前期访问的数据,累计次数很大,新来的缓存数据累加时间短。老的记录占用缓存,过去的一些大量被访问的记录,在之后不一定会继续使用,一直占用位置。突发的稀疏流量,很容易被淘汰。导致缓存命中率下降。

4、TinyLFU算法

TinyLFU是为了解决LFU上面的三个缺点而设计出来的。

(1)减少访问频率的存储的空间开销 和 访问记录更新的时间开销 采用Count–Min Sketch 算法。Count–Min Sketch算法类似布隆过滤器,基于概率统计,存储访问频率,节省空间。

工作原理:

1.某个key计算出4个hash函数,落在不同的位置,每当元素被访问时针对hash计算找到对应的位置,将进行次数加1。

2.查询某个key的频率,需要取得4个索引位置的频率信息,取4个位置中的最小值返回。

实现方案:

 节约空间开销方式:默认访问频次最高15次(15次就算是热点数据了),用4位就可以存储15。一个long类型64位置,每个long分成16份4位。一个key对应4个hash值,一个long就可以存储4个key的访问次数。一个long类型数组大小就是容量的4倍。节约了4分之3的空间。

(2)提升对局部热点数据的算法命中率 ,使用保鲜机制,尽量让缓存保持相对"新鲜",访问越大越不新鲜,适当降低保证新鲜度。Caffeine有一个新鲜度机制,剔除掉过去频率很高,之后不经常使用的缓存。具体操作就是整体的统计计数(所有key的频率统计和的最大值)达到某一个取值时,所有记录的频率统计除以2。

TinyLFU 的算法流程:缓存空间不够时,TinyLFU找到要淘汰的元素(频率最小的元素),新元素放入缓存,替代要淘汰的元素。

5、W-TinyLFU算法

        TinyLFU在面对突发性的稀疏流量变现比较差,因为新的记录还没有建立足够多的频率就被淘汰了,导致命中率下降。W-TinyLFU是对TinyLFU的一个优化,加入了LRU应对局部突发流量,实现缓存命中率最优。W-TinyLFU= LRU(适合局部突发流量)+LFU(适合局部周期流量)。

设计图:

W-TinyLFU将缓存分为2大区域:window cache(应对局部突发流量) 和 main cache(应对局部周期流量)。main cache又拆分成Protected Cache(保护区)和Probation Cache(考察区)两个区域,这两个区域都是基于LRU的Cache。Caffeine的默认比例:LRU(1%),剩下99%中protected(80%),probation(20%)。这时整体性能和命中率表现得最好(已经过实验验证)。 在运行时可以根据统计数据(statistics)去动态调整。

W-TinyLFU算法写入流程:新的缓存项写入缓存,先写入window cache区,如果window cache区满了,根据LRU把淘汰出来的数据放到Probation(考察)区域。如果Probation区满了,与probation中通过lru淘汰的数据进行使用TinyLFU pk。胜者留在Probation区域,输者被淘汰。Probation中的数据被访问了,则移动到Protected。如果Protected满了,protected根据LRU淘汰到probation。

优点:

  1. 使用Count-Min Sketch算法存储访问频率,极大的节省空间。
  2. TinyLFU会 定期进行新鲜度提升、 访问次数的衰减操作,应对访问模式变化。
  3. 使用W-LRU机制能够提高缓存命中率。

四、Caffeine代码实现

 缓存操作执行流程:

  1. 通过put操作将数据放入data属性中(ConcurrentHashMap)。
  2. 创建AddTask任务,放入(offer)写缓存:writeBuffer。
  3. 从writeBuffer中获取任务并执行其run方法,追加记录频率:代码实现在frequencySketch().increment(key)。
  4. 往window区写入数据。
  5. 如果数据超过window区大小,将数据移到probation区。
  6. 比较从window区晋升的数据和probation区的老数据的频率,输者被淘汰,从data中删除。

相关代码:

1.Count–Min Sketch算法写入频率 和 查询频率 和 保新机制 代码实现:

//1.Count–Min Sketch 算法 写入频率 使用FrequencySketch.increment(key)方法实现:
public void increment(@NonNull E e) {
    if (!this.isNotInitialized()) {
        //根据key的hashCode通过一个哈希函数得到一个hash值
        int hash = this.spread(e.hashCode());
        //Caffeine把64long分成16份,4bit一份,start是定位到具体的份的位置。
        //用hash值低两位作为随机数,左移2位,得到小于16的值
        //start 值:0、4、8、12
        int start = (hash & 3) << 2;
        //indexOf根据hash取获得四个元素在table数组下标的index,
        int index0 = this.indexOf(hash, 0);
        int index1 = this.indexOf(hash, 1);
        int index2 = this.indexOf(hash, 2);
        int index3 = this.indexOf(hash, 3);
        //根据index和start(+1, +2, +3)的值,把table[index]对应的等分追加1
        boolean added = this.incrementAt(index0, start);
        added |= this.incrementAt(index1, start + 1);
        added |= this.incrementAt(index2, start + 2);
        added |= this.incrementAt(index3, start + 3);
        //key的访问次数是否达到有个最大值
        //size为key的访问次数之合。sampleSize为最大值。size每统计一次+1
        if (added && ++this.size == this.sampleSize) {
            this.reset();
        }
    }
}
int indexOf(int item, int i) {
    long hash = ((long)item + SEED[i]) * SEED[i];
    hash += hash >>> 32;
    return (int)hash & this.tableMask;
}
//j表示16等分的下标,index对应等分的位置
boolean incrementAt(int i, int j) {
    //offset当于在64位中的下标 16 * 4
    int offset = j << 2;
    //统计数最大为15
    long mask = 15L << offset;
    //如果结果不等于15 就追加1,等于15就不追加。
    if ((this.table[i] & mask) != mask) {
        long[] var10000 = this.table;
        var10000[i] += 1L << offset;
        return true;
    } else {
        return false;
    }
}
 
//2.查询某个key的访问次数
@NonNegative
public int frequency(@NonNull E e) {
    if (this.isNotInitialized()) {
        return 0;
    } else {
        int hash = this.spread(e.hashCode());
        //得到下标
        int start = (hash & 3) << 2;
        int frequency = 2147483647;
        //循环四次,分别获取在table数组中不同的下标位置
        for(int i = 0; i < 4; ++i) {
            int index = this.indexOf(hash, i);
            //定位到table[index] + 等分的位置,再根据mask取出计数值
            int count = (int)(this.table[index] >>> (start + i << 2) & 15L);
            //取四个中的较小值
            frequency = Math.min(frequency, count);
        }
        return frequency;
    }
}
 
//3.保新机制代码实现
void reset() {
    int count = 0;
    for(int i = 0; i < this.table.length; ++i) {
        count += Long.bitCount(this.table[i] & 1229782938247303441L);
        this.table[i] = this.table[i] >>> 1 & 8608480567731124087L;
    }
    this.size = (this.size >>> 1) - (count >>> 2);
}

2.evictEntries()淘汰方法入口:

//最大的个数限制
long maximum;
//当前的个数
long weightedSize;
//window区的最大限制
long windowMaximum;
//window区当前的个数
long windowWeightedSize;
//protected区的最大限制
long mainProtectedMaximum;
//protected区当前的个数
long mainProtectedWeightedSize;
 
//1.window区的LRU queue 缓存1%的数据
@GuardedBy("evictionLock")
protected AccessOrderDeque<Node<K, V>> accessOrderWindowDeque() {
  throw new UnsupportedOperationException();
}
//probation区的LRU queue 缓存main内数据的20%
@GuardedBy("evictionLock")
protected AccessOrderDeque<Node<K, V>> accessOrderProbationDeque() {
  throw new UnsupportedOperationException();
}
//protected区的LRU queue 缓存main内数据的80%
@GuardedBy("evictionLock")
protected AccessOrderDeque<Node<K, V>> accessOrderProtectedDeque() {
  throw new UnsupportedOperationException();
}
 
//淘汰方法入口
@GuardedBy("evictionLock")
void evictEntries() {
    if (!evicts()) {
      return;
    }
    // 淘汰window区的记录
    //candidate  第一个 准备从Window区晋升的元素
    int candidates = evictFromWindow();
    //淘汰Main区的记录
    evictFromMain(candidates);
}
//新数据直接到窗口缓存区,超过Window区大小,需要进入到probation区
//使用LRU淘汰
@GuardedBy("evictionLock")
int evictFromWindow() {
    int candidates = 0;
    Node next;
    //判断头节点,是否超过区域最大限制,超过限制需要和probation区要淘汰的数据进行pk
    for(Node node = (Node)this.accessOrderWindowDeque().peek(); this.windowWeightedSize() > this.windowMaximum() && node != null; node = next) {
        next = node.getNextInAccessOrder();
        if (node.getPolicyWeight() != 0) {
            node.makeMainProbation();
            //window区删除node
            this.accessOrderWindowDeque().remove(node);
            //node移动到probation区
            this.accessOrderProbationDeque().add(node);
            ++candidates;
            this.setWindowWeightedSize(this.windowWeightedSize() - (long)node.getPolicyWeight());
        }
    }
    return candidates;
}
  
//主缓存取 数据进入probation队列后,如果超过最大容量,需要淘汰数据
@GuardedBy("evictionLock")
void evictFromMain(int candidates) {
    int victimQueue = 1;
    //victim是probation queue的头部
    Node<K, V> victim = (Node)this.accessOrderProbationDeque().peekFirst();
    //candidate是probation queue的尾部(刚晋升过来的数据)
    Node candidate = (Node)this.accessOrderProbationDeque().peekLast();
    //当缓存容量不够时需要处理
    while(this.weightedSize() > this.maximum()) {
        if (candidates == 0) {
            candidate = null;
        }
        if (candidate == null && victim == null) {
            if (victimQueue == 1) {
                victim = (Node)this.accessOrderProtectedDeque().peekFirst();
                victimQueue = 2;
            } else {
                if (victimQueue != 2) {
                    break;
                }
  
                victim = (Node)this.accessOrderWindowDeque().peekFirst();
                victimQueue = 0;
            }
        } else if (victim != null && victim.getPolicyWeight() == 0) {
            victim = victim.getNextInAccessOrder();
        } else if (candidate != null && candidate.getPolicyWeight() == 0) {
            candidate = candidate.getPreviousInAccessOrder();
            --candidates;
        } else {
            Node evict;
            if (victim == null) {
                evict = candidate.getPreviousInAccessOrder();
                Node<K, V> evict = candidate;
                candidate = evict;
                --candidates;
                this.evictEntry(evict, RemovalCause.SIZE, 0L);
            } else if (candidate == null) {
                evict = victim;
                victim = victim.getNextInAccessOrder();
                this.evictEntry(evict, RemovalCause.SIZE, 0L);
            } else {
                K victimKey = victim.getKey();
                K candidateKey = candidate.getKey();
                Node evict;
                if (victimKey == null) {
                    evict = victim;
                    victim = victim.getNextInAccessOrder();
                    this.evictEntry(evict, RemovalCause.COLLECTED, 0L);
                } else if (candidateKey == null) {
                    --candidates;
                    evict = candidate;
                    candidate = candidate.getPreviousInAccessOrder();
                    this.evictEntry(evict, RemovalCause.COLLECTED, 0L);
                } else if ((long)candidate.getPolicyWeight() > this.maximum()) {
                    //节点超过最大个数限制直接淘汰
                    --candidates;
                    evict = candidate;
                    candidate = candidate.getPreviousInAccessOrder();
                    this.evictEntry(evict, RemovalCause.SIZE, 0L);
                } else {
                    --candidates;
                    //根据数据victimKey和candidateKey访问频率比较,失败的淘汰
                    if (this.admit(candidateKey, victimKey)) {
                        //candidateKey胜出,淘汰victimKey
                        evict = victim;
                        victim = victim.getNextInAccessOrder();
                        this.evictEntry(evict, RemovalCause.SIZE, 0L);
                        candidate = candidate.getPreviousInAccessOrder();
                    } else {
                        //victimKey胜出,淘汰candidateKey
                        evict = candidate;
                        candidate = candidate.getPreviousInAccessOrder();
                        this.evictEntry(evict, RemovalCause.SIZE, 0L);
                    }
                }
            }
        }
    }
}
 
//在window队列通过lru淘汰出来的"候选者candidate"与
//probation队列通过lru淘汰出来的"被驱逐者victim"进行频率比较,失败者将被从cache中真正移除。
@GuardedBy("evictionLock")
boolean admit(K candidateKey, K victimKey) {
    int victimFreq = this.frequencySketch().frequency(victimKey);
    int candidateFreq = this.frequencySketch().frequency(candidateKey);
    //频率高获胜
    if (candidateFreq > victimFreq) {
        return true;
    } else if (candidateFreq <= 5) {
        return false;
    } else {
        //随机淘汰 (为了防止hash冲突攻击)
        int random = ThreadLocalRandom.current().nextInt();
        return (random & 127) == 0;
    }
}
 
//probation区域的数据移动到protected区域过程
//当数据被访问时并且该数据在probation中,这个数据就会移动到protected中去,
//同时通过lru从protected中淘汰一个数据进入到probation中
/** Promote the node from probation to protected on an access. */
@GuardedBy("evictionLock")
void reorderProbation(Node<K, V> node) {
  if (!accessOrderProbationDeque().contains(node)) {
    // Ignore stale accesses for an entry that is no longer present
    return;
  } else if (node.getPolicyWeight() > mainProtectedMaximum()) {
    return;
  }
  // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
  // This is deferred to the adaption phase at the end of the maintenance cycle.
  setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
  accessOrderProbationDeque().remove(node);
  accessOrderProtectedDeque().add(node);
  node.makeMainProtected();
}

3.动态调整window区和protected区的大小 climb()方法:

//与上次命中率之差的阈值
static final double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d;
//步长(调整)的大小(跟最大值maximum的比例)
static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;
//步长的衰减比例
static final double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d;
  /** Adapts the eviction policy to towards the optimal recency / frequency configuration. */
//climb方法的主要作用就是动态调整window区的大小(相应的,main区的大小也会发生变化,两个之和为100%)。
//因为区域的大小发生了变化,那么区域内的数据也可能需要发生相应的移动。
@GuardedBy("evictionLock")
void climb() {
  if (!evicts()) {
    return;
  }
  //确定window需要调整的大小
  determineAdjustment();
  //如果protected区有溢出,把溢出部分移动到probation区。因为下面的操作有可能需要调整到protected区。
  demoteFromMainProtected();
  long amount = adjustment();
  if (amount == 0) {
    return;
  } else if (amount > 0) {
    //增加window的大小
    increaseWindow();
  } else {
    //减少window的大小
    decreaseWindow();
  }
}

@GuardedBy("evictionLock")
void determineAdjustment() {
  //如果frequencySketch还没初始化,则返回
  if (frequencySketch().isNotInitialized()) {
    setPreviousSampleHitRate(0.0);
    setMissesInSample(0);
    setHitsInSample(0);
    return;
  }
  //总请求量 = 命中 + miss
  int requestCount = hitsInSample() + missesInSample();
  //默认下sampleSize = 10 * maximum。没达到最大值返回。
  if (requestCount < frequencySketch().sampleSize) {
    return;
  }
  //命中率的公式 = 命中 / 总请求
  double hitRate = (double) hitsInSample() / requestCount;
  //命中率的差值
  double hitRateChange = hitRate - previousSampleHitRate();
  //本次调整的大小,是由命中率的差值和上次的stepSize决定的
  double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();
  //下次的调整大小:如果命中率的之差大于0.05,则重置为0.065 * maximum,否则按照0.98来进行衰减
  double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)
      ? HILL_CLIMBER_STEP_PERCENT * maximum() * (amount >= 0 ? 1 : -1)
      : HILL_CLIMBER_STEP_DECAY_RATE * amount;
  setPreviousSampleHitRate(hitRate);
  setAdjustment((long) amount);
  setStepSize(nextStepSize);
  setMissesInSample(0);
  setHitsInSample(0);
}

//减少protected区溢出的部分
@GuardedBy("evictionLock")
void demoteFromMainProtected() {
  long mainProtectedMaximum = mainProtectedMaximum();
  long mainProtectedWeightedSize = mainProtectedWeightedSize();
  if (mainProtectedWeightedSize <= mainProtectedMaximum) {
    return;
  }
  for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
    if (mainProtectedWeightedSize <= mainProtectedMaximum) {
      break;
    }
    Node<K, V> demoted = accessOrderProtectedDeque().poll();
    if (demoted == null) {
      break;
    }
    demoted.makeMainProbation();
    accessOrderProbationDeque().add(demoted);
    mainProtectedWeightedSize -= demoted.getPolicyWeight();
  }
  setMainProtectedWeightedSize(mainProtectedWeightedSize);
}

//增加window区的大小
@GuardedBy("evictionLock")
void increaseWindow() {
  if (mainProtectedMaximum() == 0) {
    return;
  }
  long quota = Math.min(adjustment(), mainProtectedMaximum());
  setMainProtectedMaximum(mainProtectedMaximum() - quota);
  setWindowMaximum(windowMaximum() + quota);
  demoteFromMainProtected();
  for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
    Node<K, V> candidate = accessOrderProbationDeque().peek();
    boolean probation = true;
    if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
      candidate = accessOrderProtectedDeque().peek();
      probation = false;
    }
    if (candidate == null) {
      break;
    }
    int weight = candidate.getPolicyWeight();
    if (quota < weight) {
      break;
    }
    quota -= weight;
    if (probation) {
      accessOrderProbationDeque().remove(candidate);
    } else {
      setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
      accessOrderProtectedDeque().remove(candidate);
    }
    setWindowWeightedSize(windowWeightedSize() + weight);
    accessOrderWindowDeque().add(candidate);
    candidate.makeWindow();
  }
  setMainProtectedMaximum(mainProtectedMaximum() + quota);
  setWindowMaximum(windowMaximum() - quota);
  setAdjustment(quota);
}

//和increaseWindow方法差不多
@GuardedBy("evictionLock")
void decreaseWindow() {
  if (windowMaximum() <= 1) {
    return;
  }
  long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
  setMainProtectedMaximum(mainProtectedMaximum() + quota);
  setWindowMaximum(windowMaximum() - quota);
  for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
    Node<K, V> candidate = accessOrderWindowDeque().peek();
    if (candidate == null) {
      break;
    }
    int weight = candidate.getPolicyWeight();
    if (quota < weight) {
      break;
    }
    quota -= weight;
    setMainProtectedWeightedSize(mainProtectedWeightedSize() + weight);
    setWindowWeightedSize(windowWeightedSize() - weight);
    accessOrderWindowDeque().remove(candidate);
    accessOrderProbationDeque().add(candidate);
    candidate.makeMainProbation();
  }
  setMainProtectedMaximum(mainProtectedMaximum() - quota);
  setWindowMaximum(windowMaximum() + quota);
  setAdjustment(-quota);
}

参考资料:

https://mp.weixin.qq.com/s/rbDdlX-ug_1KheUM2eRdzQ

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值