網(wǎng)站首頁 編程語言 正文
簡介
前置知識
知道什么是緩存
聽完本節(jié)公開課,你可以收獲
- 掌握樸素LRU、LFU算法的思想以及源碼
- 掌握一種流式計數(shù)的算法 Count-Min Sketch
- 手撕TinyLFU算法、分析Window-TinyLFU源碼
一、LRU和LFU算法
LRU算法
LRU Least Recently Used 最近最少使用算法
LRU 算法的思想是如果一個數(shù)據(jù)在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。所以,當指定的空間已存滿數(shù)據(jù)時,應(yīng)當把最久沒有被訪問到的數(shù)據(jù)淘汰。
也就是淘汰數(shù)據(jù)的時候,只看數(shù)據(jù)在緩存里面待的時間長短這個維度。
這樣子做有什么缺點呢?我們來看個例子
無法復(fù)制加載中的內(nèi)容
按照LRU算法進行訪問和數(shù)據(jù)淘汰,10次訪問的結(jié)果如下圖所示
無法復(fù)制加載中的內(nèi)容
10次訪問結(jié)束后,緩存中剩下的數(shù)據(jù)是b、c、d三個元素,這個顯然不太合理。
直觀上講,為什么說他不合理,是因為明明a是被頻繁訪問的數(shù)據(jù),最終卻被淘汰掉了。所以如果要改進這個算法,我們希望的是能夠記錄每個元素的訪問頻率信息,訪問頻率最低的那個才是最應(yīng)該被淘汰的那個。
恭喜你,這就是LFU的規(guī)則。
在開始LFU之前,我們先來看一下LRU的代碼怎么寫。
有句古話講得好:緩存就是Map + 淘汰策略。Map的作用是提供快速訪問,淘汰策略是緩存算法的靈魂,決定了命中率的高低。根據(jù)對于LRU的描述,我們需要一個東西(術(shù)語叫做數(shù)據(jù)結(jié)構(gòu))來記錄數(shù)據(jù)被訪問的先后順序,這里我們可以選擇鏈表。
打開IDE,迅速寫下第一行代碼:
type LRU struct { data map[string]*list.Element cap int list *list.List }
解釋一下為什么需要這幾個變量, cap 是緩存中可以存放的數(shù)據(jù)個數(shù),也就是緩存的容量上限。data就是Map。List我們用來記錄數(shù)據(jù)的先后訪問順序,每次訪問,都把本次訪問的節(jié)點移動到鏈表中的頭部。這樣子整個鏈表就會按照近期的訪問記錄來排序了。
func (lru *LRU) add(k, v string) { if Map中存有這條Key { 替換Map中的Value值 將鏈表中的對應(yīng)節(jié)點移到最前面 } else { if 已經(jīng)達到緩存容量上限 { 獲取鏈表尾部節(jié)點的Key,并從Map中刪除 移除鏈表尾部的Node } 創(chuàng)建要插入的新節(jié)點 將新節(jié)點插入到鏈表頭部 放入Map中 } } func (lru *LRU) get(k string) string { if Map中存有這條Key { 返回查詢到的Value 將對應(yīng)節(jié)點移動到鏈表頭部 } else { 返回 空 } }
LFU算法
我們已經(jīng)成功的寫出了LRU算法(偽代碼),接下來嘗試自己寫一下LFU算法。首先我們知道LFU算法比LRU多了什么,LFU需要記錄每條數(shù)據(jù)的訪問次數(shù)信息,并且按照訪問次數(shù)從高到低排序,訪問次數(shù)用什么來記錄呢?
只需要在鏈表節(jié)點中增加一個訪問頻率Frequency,就可以了,這個Frequency可以使用int來存儲。同時排序的規(guī)則稍加變動,不是把最近訪問的放到最前面,而是按照訪問頻率插入到對應(yīng)位置即可。如果頻率相同,再按照LRU的規(guī)則,比較誰是最新訪問的。
暫時無法在文檔外展示此內(nèi)容
小結(jié):
講完了LRU和LFU,我們來看一下他們有啥優(yōu)缺點。
LRU
優(yōu)點:實現(xiàn)簡單、可以很快的適應(yīng)訪問模式的改變
缺點:對于熱點數(shù)據(jù)的命中率可能不如LFU
LFU
優(yōu)點:對于熱點數(shù)據(jù)命中率更高
缺點:難以應(yīng)對突發(fā)的稀疏流量、可能存在舊數(shù)據(jù)長期不被淘汰,會影響某些場景下的命中率(如外賣),需要額外消耗來記錄和更新訪問頻率
二、TinyLFU
Count-Min Sketch 算法
剛才提到了LFU需要統(tǒng)計每個條數(shù)據(jù)的訪問頻率,這就需要一個int或者long類型來存儲次數(shù),但是仔細一想,一條緩存數(shù)據(jù)的訪問次數(shù)真的需要int類型這么大的表示范圍來統(tǒng)計嗎?我們認為一個緩存被訪問15次已經(jīng)算是很高的頻率了,那么我們只用4個Bit就可以保存這個數(shù)據(jù)。(2^4=16)
再來介紹一個cmSketch算法,看過硬核課堂BloomFilter視頻的都知道,BloomFilter利用位圖的思想來標記一條數(shù)據(jù)是否存在,存在與否可以用某個Bit位的0 | 1來代替,那么我們能不能擴展一下,利用這種思想來計數(shù)呢?
我們給要計數(shù)的值計算一個Hash,然后在位圖中給這個Hash值對應(yīng)的位置累加1就可以了,但是BloomFilter中的一個典型問題是假陽性,可以說只要是用Hash計算就有存在沖突的可能,那么cmSketch計數(shù)法如果出現(xiàn)沖突會怎么樣呢?會給同一個位置多計算訪問次數(shù)。這里cmSketch選擇了以最小的統(tǒng)計數(shù)據(jù)值作為結(jié)果。這是一個不那么精確地統(tǒng)計方法,但是可以大致的反應(yīng)訪問分布的規(guī)律。
因為這個算法也就有了一個名字,叫做Count-Min Sketch。
下面我們來手撕這個算法。
//根據(jù)BloomFilter來思考一下我們需要什么 //一個bit圖,n個Hash函數(shù) //一個BitMap的實現(xiàn) type cmRow []byte //byte = uint8 = 0000,0000 = COUNTER 4BIT = 2 counter //64 counter //1 uint8 = 2counter //32 uint8 = 64 counter func newCmRow(numCounters int64) cmRow { return make(cmRow, numCounters/2) } func (r cmRow) get(n uint64) byte { return byte(r[n/2]>>((n&1)*4)) & 0x0f } 0000,0000|0000,0000| 0000,0000 make([]byte, 3) = 6 counter func (r cmRow) increment(n uint64) { //定位到第i個Counter i := n / 2 //r[i] //右移距離,偶數(shù)為0,奇數(shù)為4 s := (n & 1) * 4 //取前4Bit還是后4Bit v := (r[i] >> s) & 0x0f //0000, 1111 //沒有超出最大計數(shù)時,計數(shù)+1 if v < 15 { r[i] += 1 << s } } //cmRow 100, //保鮮 func (r cmRow) reset() { // 計數(shù)減半 for i := range r { r[i] = (r[i] >> 1) & 0x77 //0111,0111 } } func (r cmRow) clear() { // 清空計數(shù) for i := range r { r[i] = 0 } } //快速計算最接近x的二次冪的算法 //比如x=5,返回8 //x = 110,返回128 //2^n //1000000 (n個0) //01111111(n個1) + 1 // x = 1001010 = 1111111 + 1 =10000000 func next2Power(x int64) int64 { x-- x |= x >> 1 x |= x >> 2 x |= x >> 4 x |= x >> 8 x |= x >> 16 x |= x >> 32 x++ return x }
如果我們要給n個數(shù)據(jù)計數(shù),那么每4Bit當做一個計數(shù)器Counter,我們一共需要幾個uint8來計數(shù)呢?答案是n/2
0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 |
---|---|---|---|---|---|---|---|
0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 |
0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 |
0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 |
//cmSketch封裝 const cmDepth = 4 type cmSketch struct { rows [cmDepth]cmRow seed [cmDepth]uint64 mask uint64 } //numCounter - 1 = next2Power() = 0111111(n個1) //0000,0000|0000,0000|0000,0000 //0000,0000|0000,0000|0000,0000 //0000,0000|0000,0000|0000,0000 //0000,0000|0000,0000|0000,0000 func newCmSketch(numCounters int64) *cmSketch { if numCounters == 0 { panic("cmSketch: bad numCounters") } numCounters = next2Power(numCounters) sketch := &cmSketch{mask: uint64(numCounters - 1)} // Initialize rows of counters and seeds. source := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < cmDepth; i++ { sketch.seed[i] = source.Uint64() sketch.rows[i] = newCmRow(numCounters) } return sketch } func (s *cmSketch) Increment(hashed uint64) { for i := range s.rows { s.rows[i].increment((hashed ^ s.seed[i]) & s.mask) } } // 找到最小的計數(shù)值 func (s *cmSketch) Estimate(hashed uint64) int64 { min := byte(255) for i := range s.rows { val := s.rows[i].get((hashed ^ s.seed[i]) & s.mask) if val < min { min = val } } return int64(min) } // 讓所有計數(shù)器都減半,保鮮機制 func (s *cmSketch) Reset() { for _, r := range s.rows { r.reset() } } // 清空所有計數(shù)器 func (s *cmSketch) Clear() { for _, r := range s.rows { r.clear() } }
TinyLFU解決了LFU統(tǒng)計的內(nèi)存消耗問題,和緩存保鮮的問題,但是TinyLFU是否還有缺點呢?
有,論文中是這么描述的,根據(jù)實測TinyLFU應(yīng)對突發(fā)的稀疏流量時表現(xiàn)不佳。大概思考一下也可以得知,這些稀疏流量的訪問頻次不足以讓他們在LFU緩存中占據(jù)位置,很快就又被淘汰了。
我們回顧之前講過的,LRU對于稀疏流量效果很好,那可以不可以把LRU和LFU結(jié)合一下呢?就出現(xiàn)了下面這種緩存策略。
三、Window-TinyLFU
Window-TinyLFU策略里包含LRU和LFU兩部分,前端的小LRU叫做Window LRU,它的容量只占據(jù)1%的總空間,它的目的就是用來存放短期的突發(fā)訪問數(shù)據(jù)。存放主要元素的Segmented LRU(SLRU)是一種LRU的改進,主要把在一個時間窗口內(nèi)命中至少2次的記錄和命中1次的單獨存放,這樣就可以把短期內(nèi)較頻繁的緩存元素區(qū)分開來。具體做法上,SLRU包含2個固定尺寸的LRU,一個叫Probation段A1,一個叫Protection段A2。新記錄總是插入到A1中,當A1的記錄被再次訪問,就把它移到A2,當A2滿了需要驅(qū)逐記錄時,會把驅(qū)逐記錄插入到A1中。W-TinyLFU中,SLRU有80%空間被分配給A2段。
視頻講解
原文鏈接:https://juejin.cn/post/7074533777661231141
相關(guān)推薦
- 2022-06-22 Python實現(xiàn)npy/mat文件的保存與讀取_python
- 2022-12-10 深入分析React源碼中的合成事件_React
- 2022-05-10 一文帶你了解中Typescript中type與interface的區(qū)別
- 2022-07-04 python如何生成任意n階的三對角矩陣_python
- 2022-02-09 C++解決輸出鏈表中倒數(shù)k個結(jié)點的問題_C 語言
- 2022-09-14 jQuery實現(xiàn)簡單計算器_jquery
- 2022-09-22 數(shù)據(jù)結(jié)構(gòu):順序表和鏈表學(xué)習(xí)小結(jié)
- 2022-08-19 解決:curl: (35) LibreSSL SSL_connect: SSL_ERROR_SYSC
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支