快取
是我們寫程式碼過程中常用的一種手段,是一種空間換時間的做法。就拿我們經常使用的 HTTP 協議,其中也存在強快取和協商快取兩種快取方式。當我們開啟一個網站的時候,瀏覽器會查詢該請求的響應頭,通過判斷響應頭中是否有 Cache-Control
、Last-Modified
、ETag
等欄位,來確定是否直接使用之前下載的資源快取,而不是重新從伺服器進行下載。
下面就是當我們訪問百度時,某些資源命中了協商快取,服務端返回 304
狀態碼,還有一部分資源命中了強快取,直接讀取了本地快取。
但是,快取並不是無限制的,會有大小的限制。無論是我們的 cookie
(不同瀏覽器有所區別,一般在 4KB
左右),還是 localStorage
(和 cookie
一樣,不同瀏覽器有所區別,有些瀏覽器為 5MB
,有些瀏覽器為 10MB
),都會有大小限制。
這個時候就需要涉及到一種演算法,需要將超出大小限制的快取進行淘汰,一般的規則是淘汰掉最近沒有被訪問到的快取,也就是今天要介紹的主角:LRU (Least recently used
:最近最少使用)。當然除了 LRU,常見的快取淘汰還有 FIFO(first-in, first-out
:先進先出) 和 LFU(Least frequently used
:最少使用)。
什麼是 LRU?
LRU (Least recently used
:最近最少使用)演算法在快取寫滿的時候,會根據所有資料的訪問記錄,淘汰掉未來被訪問機率最低的資料。也就是說該演算法認為,最近被訪問過的資料,在將來被訪問的機率最大。
為了方便理解 LRU 演算法的全流程,畫了一個簡單的圖:
- 假設我們有一塊記憶體,一共能夠儲存 5 資料塊;
- 依次向記憶體存入A、B、C、D、E,此時記憶體已經存滿;
- 再次插入新的資料時,會將在記憶體存放時間最久的資料A淘汰掉;
- 當我們在外部再次讀取資料B時,已經處於末尾的B會被標記為活躍狀態,提到頭部,資料C就變成了存放時間最久的資料;
- 再次插入新的資料G,存放時間最久的資料C就會被淘汰掉;
演算法實現
下面通過一段簡單的程式碼來實現這個邏輯。
class LRUCache {
list = [] // 用於標記先後順序
cache = {} // 用於快取所有資料
capacity = 0 // 快取的最大容量
constructor (capacity) {
// 儲存 LRU 可快取的最大容量
this.capacity = capacity
}
}
基本的結構如上所示,LRU需要實現的就是兩個方法:get
和 put
。
class LRUCache {
// 獲取資料
get (key) { }
// 儲存資料
put (key, value) { }
}
我們現在看看如何進行資料的儲存:
class LRUCache {
// 儲存資料
put (key, value) {
// 儲存之前需要先判斷長度是否達到上限
if (this.list.length >= this.capacity) {
// 由於每次儲存後,都會將 key 放入 list 最後,
// 所以,需要取出第一個 key,並刪除cache中的資料。
const latest = this.list.shift()
delete this.cache[latest]
}
// 寫入快取
this.cache[key] = value
// 寫入快取後,需要將 key 放入 list 的最後
this.list.push(key)
}
}
然後,在每次獲取資料時,都需要更新 list
,將當前獲取的 key
放到 list
的最後。
class LRUCache {
// 獲取資料
get (key) {
if (this.cache[key] !== undefined) {
// 如果 key 對應的快取存在
// 在返回快取之前,需要重新啟用 key
this.active(key)
return this.cache[key]
}
return undefined
}
// 重新啟用key,將指定 key 移動到 list 最後
active (key) {
// 先將 key 在 list 中刪除
const idx = this.list.indexOf(key)
if (idx !== -1) {
this.list.splice(idx, 1)
}
// 然後將 key 放到 list 最後面
this.list.push(key)
}
}
這個時候,其實還沒有完全實現,因為除了 get
操作,put
操作也需要將對應的 key
重新啟用。
class LRUCache {
// 儲存資料
put (key, value) {
if (this.cache[key]) {
// 如果該 key 之前存在,將 key 重新啟用
this.active(key)
this.cache[key] = value
// 而且此時快取的長度不會發生變化
// 所以不需要進行後續的長度判斷,可以直接返回
return
}
// 儲存之前需要先判斷長度是否達到上限
if (this.list.length >= this.capacity) {
// 由於每次儲存後,都會將 key 放入 list 最後,
// 所以,需要取出第一個 key,並刪除cache中的資料。
const latest = this.list.shift()
delete this.cache[latest]
}
// 寫入快取
this.cache[key] = value
// 寫入快取後,需要將 key 放入 list 的最後
this.list.push(key)
}
}
可能會有人覺得這種演算法在前端沒有什麼應用場景,說起來,在 Vue 的內建元件 keep-alive
中就使用到了 LRU
演算法。
後續應該還會繼續介紹一下 LFU
演算法,敬請期待……