自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一篇學(xué)會(huì)Caffeine W-TinyLFU源碼分析

開發(fā) 前端
Caffeine使用一個(gè)ConcurrencyHashMap來保存所有數(shù)據(jù),那它的過期淘汰策略采用什么方式與數(shù)據(jù)結(jié)構(gòu)呢?其中寫過期是使用writeOrderDeque,這個(gè)比較簡單無需多說,而讀過期相對復(fù)雜很多,使用W-TinyLFU的結(jié)構(gòu)與算法。

[[410834]]

本文轉(zhuǎn)載自微信公眾號「肌肉碼農(nóng)」,作者肌肉碼農(nóng)。轉(zhuǎn)載本文請聯(lián)系肌肉碼農(nóng)公眾號。

Caffeine使用一個(gè)ConcurrencyHashMap來保存所有數(shù)據(jù),那它的過期淘汰策略采用什么方式與數(shù)據(jù)結(jié)構(gòu)呢?其中寫過期是使用writeOrderDeque,這個(gè)比較簡單無需多說,而讀過期相對復(fù)雜很多,使用W-TinyLFU的結(jié)構(gòu)與算法。

網(wǎng)絡(luò)上有很多文章介紹W-TinyLFU結(jié)構(gòu)的,大家可以去查一下,這里主要是從源碼來分析,總的來說它使用了三個(gè)雙端隊(duì)列:accessOrderEdenDeque,accessOrderProbationDeque,accessOrderProtectedDeque,使用雙端隊(duì)列的原因是支持LRU算法比較方便。

accessOrderEdenDeque屬于eden區(qū),緩存1%的數(shù)據(jù),其余的99%緩存在main區(qū)。

accessOrderProbationDeque屬于main區(qū),緩存main內(nèi)數(shù)據(jù)的20%,這部分是屬于冷數(shù)據(jù),即將補(bǔ)淘汰。

accessOrderProtectedDeque屬于main區(qū),緩存main內(nèi)數(shù)據(jù)的80%,這部分是屬于熱數(shù)據(jù),是整個(gè)緩存的主存區(qū)。

我們先看一下淘汰方法入口:

  1. void evictEntries() { 
  2.   if (!evicts()) { 
  3.     return
  4.   } 
  5.   //先從edn區(qū)淘汰 
  6.   int candidates = evictFromEden(); 
  7.   //eden淘汰后的數(shù)據(jù)進(jìn)入main區(qū),然后再從main區(qū)淘汰 
  8.   evictFromMain(candidates); 

accessOrderEdenDeque對應(yīng)W-TinyLFU的W(window),這里保存的是最新寫入數(shù)據(jù)的引用,它使用LRU淘汰,這里面的數(shù)據(jù)主要是應(yīng)對突發(fā)流量的問題,淘汰后的數(shù)據(jù)進(jìn)入accessOrderProbationDeque.代碼如下:

  1. int evictFromEden() { 
  2.   int candidates = 0; 
  3.   Node<K, V> node = accessOrderEdenDeque().peek(); 
  4.   while (edenWeightedSize() > edenMaximum()) { 
  5.     // The pending operations will adjust the size to reflect the correct weight 
  6.     if (node == null) { 
  7.       break; 
  8.     } 
  9.  
  10.     Node<K, V> next = node.getNextInAccessOrder(); 
  11.     if (node.getWeight() != 0) { 
  12.       node.makeMainProbation(); 
  13.       //先從eden區(qū)移除 
  14.       accessOrderEdenDeque().remove(node); 
  15.       //移除的數(shù)據(jù)加入到main區(qū)的probation隊(duì)列 
  16.       accessOrderProbationDeque().add(node); 
  17.       candidates++; 
  18.  
  19.       lazySetEdenWeightedSize(edenWeightedSize() - node.getPolicyWeight()); 
  20.     } 
  21.     node = next
  22.   } 
  23.  
  24.   return candidates; 

數(shù)據(jù)進(jìn)入probation隊(duì)列后,繼續(xù)執(zhí)行以下代碼:

  1. void evictFromMain(int candidates) { 
  2.   int victimQueue = PROBATION; 
  3.   Node<K, V> victim = accessOrderProbationDeque().peekFirst(); 
  4.   Node<K, V> candidate = accessOrderProbationDeque().peekLast(); 
  5.   while (weightedSize() > maximum()) { 
  6.     // Stop trying to evict candidates and always prefer the victim 
  7.     if (candidates == 0) { 
  8.       candidate = null
  9.     } 
  10.  
  11.     // Try evicting from the protected and eden queues 
  12.     if ((candidate == null) && (victim == null)) { 
  13.       if (victimQueue == PROBATION) { 
  14.         victim = accessOrderProtectedDeque().peekFirst(); 
  15.         victimQueue = PROTECTED; 
  16.         continue
  17.       } else if (victimQueue == PROTECTED) { 
  18.         victim = accessOrderEdenDeque().peekFirst(); 
  19.         victimQueue = EDEN; 
  20.         continue
  21.       } 
  22.  
  23.       // The pending operations will adjust the size to reflect the correct weight 
  24.       break; 
  25.     } 
  26.  
  27.     // Skip over entries with zero weight 
  28.     if ((victim != null) && (victim.getPolicyWeight() == 0)) { 
  29.       victim = victim.getNextInAccessOrder(); 
  30.       continue
  31.     } else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) { 
  32.       candidate = candidate.getPreviousInAccessOrder(); 
  33.       candidates--; 
  34.       continue
  35.     } 
  36.  
  37.     // Evict immediately if only one of the entries is present 
  38.     if (victim == null) { 
  39.       candidates--; 
  40.       Node<K, V> evict = candidate; 
  41.       candidate = candidate.getPreviousInAccessOrder(); 
  42.       evictEntry(evict, RemovalCause.SIZE, 0L); 
  43.       continue
  44.     } else if (candidate == null) { 
  45.       Node<K, V> evict = victim; 
  46.       victim = victim.getNextInAccessOrder(); 
  47.       evictEntry(evict, RemovalCause.SIZE, 0L); 
  48.       continue
  49.     } 
  50.  
  51.     // Evict immediately if an entry was collected 
  52.     K victimKey = victim.getKey(); 
  53.     K candidateKey = candidate.getKey(); 
  54.     if (victimKey == null) { 
  55.       Node<K, V> evict = victim; 
  56.       victim = victim.getNextInAccessOrder(); 
  57.       evictEntry(evict, RemovalCause.COLLECTED, 0L); 
  58.       continue
  59.     } else if (candidateKey == null) { 
  60.       candidates--; 
  61.       Node<K, V> evict = candidate; 
  62.       candidate = candidate.getPreviousInAccessOrder(); 
  63.       evictEntry(evict, RemovalCause.COLLECTED, 0L); 
  64.       continue
  65.     } 
  66.  
  67.     // Evict immediately if the candidate's weight exceeds the maximum 
  68.     if (candidate.getPolicyWeight() > maximum()) { 
  69.       candidates--; 
  70.       Node<K, V> evict = candidate; 
  71.       candidate = candidate.getPreviousInAccessOrder(); 
  72.       evictEntry(evict, RemovalCause.SIZE, 0L); 
  73.       continue
  74.     } 
  75.  
  76.     // Evict the entry with the lowest frequency 
  77.     candidates--; 
  78.     //最核心算法在這里:從probation的頭尾取出兩個(gè)node進(jìn)行比較頻率,頻率更小者將被remove 
  79.     if (admit(candidateKey, victimKey)) { 
  80.       Node<K, V> evict = victim; 
  81.       victim = victim.getNextInAccessOrder(); 
  82.       evictEntry(evict, RemovalCause.SIZE, 0L); 
  83.       candidate = candidate.getPreviousInAccessOrder(); 
  84.     } else { 
  85.       Node<K, V> evict = candidate; 
  86.       candidate = candidate.getPreviousInAccessOrder(); 
  87.       evictEntry(evict, RemovalCause.SIZE, 0L); 
  88.     } 
  89.   } 

上面的代碼邏輯是從probation的頭尾取出兩個(gè)node進(jìn)行比較頻率,頻率更小者將被remove,其中尾部元素就是上一部分從eden中淘汰出來的元素,如果將兩步邏輯合并起來講是這樣的:在eden隊(duì)列通過lru淘汰出來的”候選者“與probation隊(duì)列通過lru淘汰出來的“被驅(qū)逐者“進(jìn)行頻率比較,失敗者將被從cache中真正移除。下面看一下它的比較邏輯admit:

  1. boolean admit(K candidateKey, K victimKey) { 
  2.   int victimFreq = frequencySketch().frequency(victimKey); 
  3.   int candidateFreq = frequencySketch().frequency(candidateKey); 
  4.   //如果候選者的頻率高就淘汰被驅(qū)逐者 
  5.   if (candidateFreq > victimFreq) { 
  6.     return true
  7.     //如果被驅(qū)逐者比候選者的頻率高,并且候選者頻率小于等于5則淘汰者 
  8.   } else if (candidateFreq <= 5) { 
  9.     // The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack 
  10.     // exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm 
  11.     // candidate reduces the number of random acceptances to minimize the impact on the hit rate. 
  12.     return false
  13.   } 
  14.   //隨機(jī)淘汰 
  15.   int random = ThreadLocalRandom.current().nextInt(); 
  16.   return ((random & 127) == 0); 

從frequencySketch取出候選者與被驅(qū)逐者的頻率,如果候選者的頻率高就淘汰被驅(qū)逐者,如果被驅(qū)逐者比候選者的頻率高,并且候選者頻率小于等于5則淘汰者,如果前面兩個(gè)條件都不滿足則隨機(jī)淘汰。

整個(gè)過程中你是不是發(fā)現(xiàn)protectedDeque并沒有什么作用,那它是怎么作為主存區(qū)來保存大部分?jǐn)?shù)據(jù)的呢?

  1. //onAccess方法觸發(fā)該方法  
  2. void reorderProbation(Node<K, V> node) { 
  3.   if (!accessOrderProbationDeque().contains(node)) { 
  4.     // Ignore stale accesses for an entry that is no longer present 
  5.     return
  6.   } else if (node.getPolicyWeight() > mainProtectedMaximum()) { 
  7.     return
  8.   } 
  9.  
  10.   long mainProtectedWeightedSize = mainProtectedWeightedSize() + node.getPolicyWeight(); 
  11.  //先從probation中移除 
  12.  accessOrderProbationDeque().remove(node); 
  13. //加入到protected中 
  14.   accessOrderProtectedDeque().add(node); 
  15.   node.makeMainProtected(); 
  16.  
  17.   long mainProtectedMaximum = mainProtectedMaximum(); 
  18. //從protected中移除 
  19.   while (mainProtectedWeightedSize > mainProtectedMaximum) { 
  20.     Node<K, V> demoted = accessOrderProtectedDeque().pollFirst(); 
  21.     if (demoted == null) { 
  22.       break; 
  23.     } 
  24.     demoted.makeMainProbation(); 
  25.     //加入到probation中 
  26.     accessOrderProbationDeque().add(demoted); 
  27.     mainProtectedWeightedSize -= node.getPolicyWeight(); 
  28.   } 
  29.  
  30.   lazySetMainProtectedWeightedSize(mainProtectedWeightedSize); 

當(dāng)數(shù)據(jù)被訪問時(shí)并且該數(shù)據(jù)在probation中,這個(gè)數(shù)據(jù)就會(huì)移動(dòng)到protected中去,同時(shí)通過lru從protected中淘汰一個(gè)數(shù)據(jù)進(jìn)入到probation中。

這樣數(shù)據(jù)流轉(zhuǎn)的邏輯全部通了:新數(shù)據(jù)都會(huì)進(jìn)入到eden中,通過lru淘汰到probation,并與probation中通過lru淘汰的數(shù)據(jù)進(jìn)行使用頻率pk,如果勝利了就繼續(xù)留在probation中,如果失敗了就會(huì)被直接淘汰,當(dāng)這條數(shù)據(jù)被訪問了,則移動(dòng)到protected。當(dāng)其它數(shù)據(jù)被訪問了,則它可能會(huì)從protected中通過lru淘汰到probation中。

TinyLFU

傳統(tǒng)LFU一般使用key-value形式來記錄每個(gè)key的頻率,優(yōu)點(diǎn)是數(shù)據(jù)結(jié)構(gòu)非常簡單,并且能跟緩存本身的數(shù)據(jù)結(jié)構(gòu)復(fù)用,增加一個(gè)屬性記錄頻率就行了,它的缺點(diǎn)也比較明顯就是頻率這個(gè)屬性會(huì)占用很大的空間,但如果改用壓縮方式存儲頻率呢? 頻率占用空間肯定可以減少,但會(huì)引出另外一個(gè)問題:怎么從壓縮后的數(shù)據(jù)里獲得對應(yīng)key的頻率呢?

TinyLFU的解決方案是類似位圖的方法,將key取hash值獲得它的位下標(biāo),然后用這個(gè)下標(biāo)來找頻率,但位圖只有0、1兩個(gè)值,那頻率明顯可能會(huì)非常大,這要怎么處理呢? 另外使用位圖需要預(yù)占非常大的空間,這個(gè)問題怎么解決呢?

TinyLFU根據(jù)最大數(shù)據(jù)量設(shè)置生成一個(gè)long數(shù)組,然后將頻率值保存在其中的四個(gè)long的4個(gè)bit位中(4個(gè)bit位不會(huì)大于15),取頻率值時(shí)則取四個(gè)中的最小一個(gè)。

Caffeine認(rèn)為頻率大于15已經(jīng)很高了,是屬于熱數(shù)據(jù),所以它只需要4個(gè)bit位來保存,long有8個(gè)字節(jié)64位,這樣可以保存16個(gè)頻率。取hash值的后左移兩位,然后加上hash四次,這樣可以利用到16個(gè)中的13個(gè),利用率挺高的,或許有更好的算法能將16個(gè)都利用到。

  1. public void increment(@Nonnull E e) { 
  2.     if (isNotInitialized()) { 
  3.       return
  4.     } 
  5.  
  6.     int hash = spread(e.hashCode()); 
  7.     int start = (hash & 3) << 2; 
  8.  
  9.     // Loop unrolling improves throughput by 5m ops/s 
  10.     int index0 = indexOf(hash, 0); //indexOf也是一種hash方法,不過會(huì)通過tableMask來限制范圍 
  11.     int index1 = indexOf(hash, 1); 
  12.     int index2 = indexOf(hash, 2); 
  13.     int index3 = indexOf(hash, 3); 
  14.  
  15.     boolean added = incrementAt(index0, start); 
  16.     added |= incrementAt(index1, start + 1); 
  17.     added |= incrementAt(index2, start + 2); 
  18.     added |= incrementAt(index3, start + 3); 
  19.  
  20.     //當(dāng)數(shù)據(jù)寫入次數(shù)達(dá)到數(shù)據(jù)長度時(shí)就重置 
  21.     if (added && (++size == sampleSize)) { 
  22.       reset(); 
  23.     } 
  24.   } 

給對應(yīng)位置的bit位四位的Int值加1:

  1. boolean incrementAt(int i, int j) { 
  2.   int offset = j << 2; 
  3.   long mask = (0xfL << offset); 
  4.   //當(dāng)已達(dá)到15時(shí),次數(shù)不再增加 
  5.   if ((table[i] & mask) != mask) { 
  6.     table[i] += (1L << offset); 
  7.     return true
  8.   } 
  9.   return false

獲得值的方法也是通過四次hash來獲得,然后取最小值:

  1. public int frequency(@Nonnull E e) { 
  2.   if (isNotInitialized()) { 
  3.     return 0; 
  4.   } 
  5.  
  6.   int hash = spread(e.hashCode()); 
  7.   int start = (hash & 3) << 2; 
  8.   int frequency = Integer.MAX_VALUE; 
  9.   //四次hash 
  10.   for (int i = 0; i < 4; i++) { 
  11.     int index = indexOf(hash, i); 
  12.     //獲得bit位四位的Int值 
  13.     int count = (int) ((table[index] >>> ((start + i) << 2)) & 0xfL); 
  14.     //取最小值 
  15.     frequency = Math.min(frequency, count); 
  16.   } 
  17.   return frequency; 

當(dāng)數(shù)據(jù)寫入次數(shù)達(dá)到數(shù)據(jù)長度時(shí)就會(huì)將次數(shù)減半,一些冷數(shù)據(jù)在這個(gè)過程中將歸0,這樣會(huì)使hash沖突降低:

  1. void reset() { 
  2.   int count = 0; 
  3.   for (int i = 0; i < table.length; i++) { 
  4.     count += Long.bitCount(table[i] & ONE_MASK); 
  5.     table[i] = (table[i] >>> 1) & RESET_MASK; 
  6.   } 
  7.   size = (size >>> 1) - (count >>> 2); 

 

責(zé)任編輯:武曉燕 來源: 肌肉碼農(nóng)
相關(guān)推薦

2021-11-08 11:21:18

redis 淘汰算法

2021-07-02 08:51:29

源碼參數(shù)Thread

2021-10-14 10:22:19

逃逸JVM性能

2022-01-02 08:43:46

Python

2022-02-07 11:01:23

ZooKeeper

2022-06-08 11:39:29

經(jīng)營分析指標(biāo)

2021-09-22 08:37:02

pod源碼分析kubernetes

2021-07-16 22:43:10

Go并發(fā)Golang

2021-09-28 08:59:30

復(fù)原IP地址

2022-04-12 08:30:52

回調(diào)函數(shù)代碼調(diào)試

2022-10-20 07:39:26

2021-10-27 09:59:35

存儲

2022-03-11 10:21:30

IO系統(tǒng)日志

2023-03-13 21:38:08

TCP數(shù)據(jù)IP地址

2021-10-29 07:35:32

Linux 命令系統(tǒng)

2023-11-01 09:07:01

Spring裝配源碼

2022-11-14 08:17:56

2021-04-29 10:18:18

循環(huán)依賴數(shù)組

2023-01-03 08:31:54

Spring讀取器配置

2021-05-11 08:54:59

建造者模式設(shè)計(jì)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號