本文從原始碼層面分析了 redis 的快取淘汰機制,並在文章末尾描述使用 Java 實現的思路,以供參考。
相關配置
為了適配用作快取的場景,redis 支援快取淘汰(eviction)並提供相應的了配置項:
maxmemory
設定記憶體使用上限,該值不能設定為小於 1M 的容量。
選項的預設值為 0,此時系統會自行計算一個記憶體上限。
maxmemory-policy
熟悉 redis 的朋友都知道,每個資料庫維護了兩個字典:
db.dict
:資料庫中所有鍵值對,也被稱作資料庫的 keyspacedb.expires
:帶有生命週期的 key 及其對應的 TTL(存留時間),因此也被稱作 expire set
當達到記憶體使用上限maxmemory
時,可指定的清理快取所使用的策略有:
noeviction
當達到最大記憶體時直接返回錯誤,不覆蓋或逐出任何資料allkeys-lfu
淘汰整個 keyspace 中最不常用的 (LFU) 鍵 (4.0 或更高版本)allkeys-lru
淘汰整個 keyspace 最近最少使用的 (LRU) 鍵allkeys-random
淘汰整個 keyspace 中的隨機鍵volatile-ttl
淘汰 expire set 中 TTL 最短的鍵volatile-lfu
淘汰 expire set 中最不常用的鍵 (4.0 或更高版本)volatile-lru
淘汰 expire set 中最近最少使用的 (LRU) 鍵volatile-random
淘汰 expire set 中的隨機鍵
當 expire set
為空時,volatile-*
與 noeviction
行為一致。
maxmemory-samples
為了保證效能,redis 中使用的 LRU 與 LFU 演算法是一類近似實現。
簡單來說就是:演算法選擇被淘汰記錄時,不會遍歷所有記錄,而是以 隨機取樣 的方式選取部分記錄進行淘汰。
maxmemory-samples
選項控制該過程的取樣數量,增大該值會增加 CPU 開銷,但演算法效果能更逼近實際的 LRU 與 LFU 。
lazyfree-lazy-eviction
清理快取就是為了釋放記憶體,但這一過程會阻塞主執行緒,影響其他命令的執行。
當刪除某個巨型記錄(比如:包含數百條記錄的 list)時,會引起效能問題,甚至導致系統假死。
延遲釋放 機制會將巨型記錄的記憶體釋放,交由其他執行緒非同步處理,從而提高系統的效能。
開啟該選項後,可能出現使用記憶體超過 maxmemory
上限的情況。
快取淘汰機制
一個完整的快取淘汰機制需要解決兩個問題:
- 確定淘汰哪些記錄 —— 淘汰策略
- 刪除被淘汰的記錄 —— 刪除策略
淘汰策略
快取能使用的記憶體是有限的,當空間不足時,應該優先淘汰那些將來不再被訪問的資料,保留那些將來還會頻繁訪問的資料。因此淘汰演算法會圍繞 時間區域性性 原理進行設計,即:如果一個資料正在被訪問,那麼在近期很可能會被再次訪問。
為了適應快取讀多寫少的特點,實際應用中會使用雜湊表來實現快取。當需要實現某種特定的快取淘汰策略時,需要引入額外的簿記 book keeping
結構。
下面回顧 3 種最常見的快取淘汰策略。
FIFO (先進先出)
越早進入快取的資料,其不再被訪問的可能性越大。
因此在淘汰快取時,應選擇在記憶體中停留時間最長的快取記錄。
使用佇列即可實現該策略:
優點:實現簡單,適合線性訪問的場景
缺點:無法適應特定的訪問熱點,快取的命中率差
簿記開銷:時間 O(1)
,空間 O(N)
LRU (最近最少使用)
一個快取被訪問後,近期再被訪問的可能性很大。
可以記錄每個快取記錄的最近訪問時間,最近未被訪問時間最長的資料會被首先淘汰。
使用連結串列即可實現該策略:
當更新 LRU 資訊時,只需調整指標:
優點:實現簡單,能適應訪問熱點
缺點:對偶發的訪問敏感,影響命中率
簿記開銷:時間 O(1)
,空間 O(N)
LRU 改進
原始的 LRU 演算法快取的是最近訪問了 1 次的資料,因此不能很好地區分頻繁和不頻繁快取引用。
這意味著,部分冷門的低頻資料也可能進入到快取,並將原本的熱點記錄擠出快取。
為了減少偶發訪問對快取的影響,後續提出的 LRU-K 演算法作出瞭如下改進:
- 當記錄訪問次數小於 K 時,會記錄在歷史佇列中(當歷史佇列滿時,可以使用 FIFO 或 LRU 策略進行淘汰)
- 當記錄訪問次數大於等於 K 時,會被從歷史佇列中移出,並記錄到 LRU 快取中
K 值越大,快取命中率越高,但適應性差,需要經過大量訪問才能將過期的熱點記錄淘汰掉。
綜合各種因素後,實踐中常用的是 LRU-2 演算法:
優點:減少偶發訪問對快取命中率的影響
缺點:需要額外的簿記開銷
簿記開銷:時間 O(1)
,空間 O(N+M)
LFU (最不經常使用)
一個快取近期內訪問頻率越高,其再被訪問的可能性越大。
可以記錄每個快取記錄的最近一段時間的訪問頻率,訪問頻率低的資料會被首先淘汰。
實現 LFU 的一個簡單方式,是在快取記錄設定一個記錄訪問次數的計數器,然後將其放入一個小頂堆:
為了保證資料的時效性,還要以一定的時間間隔對計數器進行衰減,保證過期的熱點資料能夠被及時淘汰:
刪除策略
常見刪除策略可以分為以下幾種:
-
實時刪除:
每次增加新的記錄時,立即查詢可淘汰的記錄,如果存在則將該記錄從快取中刪除- 優點:實時性好,最節省記憶體空間
- 缺點:查詢淘汰記錄會影響寫入的效率,需要額外的簿記結構提高查詢效率(比如 LRU 中的連結串列)
-
惰性刪除:
在快取中設定兩個計數器,一個統計訪問快取的次數,一個統計可淘汰記錄的數量
每經過 N 次訪問後或當前可淘汰記錄數量大於 M,則觸發一次批量刪除(M 與 N 可調節)- 優點:對正常快取操作影響小,批量刪除減少維護開銷
- 缺點:實時性較差,偶發的刪除操作會導致訪問耗時波動
-
非同步刪除:
設定一個獨立的定時器執行緒,每隔固定的時間觸發一次批量刪除- 優點:對正常快取操作影透明,無額外效能開銷
- 缺點:需要增加維護執行緒,並且需要提前規劃快取的負載,以此決定如何在多個快取例項上排程
redis 實現
redis 中實現了 LRU 與 LFU 兩種淘汰策略
為了節省空間,redis 沒有使用前面描述的簿記結構實現 LRU 或 LFU,而是在 robj
中使用一個 24bits 的空間記錄訪問資訊:
#define LRU_BITS 24
typedef struct redisObject {
...
unsigned lru:LRU_BITS; /* LRU 時間 (相對與全域性 lru_clock 的時間) 或
* LFU 資料 (8bits 記錄訪問頻率,16 bits 記錄訪問時間). */
} robj;
每當記錄被命中時,redis 都會更新 robj.lru
作為後面淘汰演算法執行的依據:
robj *lookupKey(redisDb *db, robj *key, int flags) {
// ...
// 根據 maxmemory_policy 選擇不同的更新策略
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
LFU 與 LRU 的更新關鍵在於 updateLFU
函式與 LRU_CLOCK
巨集,下面分別進行分析。
更新 LRU 時間
當時使用 LRU 演算法時,robj.lru
記錄的是最近一次訪問的時間戳,可以據此找出長時間未被訪問的記錄。
為了減少系統呼叫,redis 設定了一個全域性的時鐘 server.lruclock
並交由後臺任務進行更新:
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
#define LRU_CLOCK_RESOLUTION 1000 /* 以毫秒為單位的時鐘精度 */
/**
* server.lruclock 的更新頻率為 1000/server.hz
* 如果該頻率高於 LRU 時鐘精度,則直接用 server.lruclock
* 避免呼叫 getLRUClock() 產生額外的開銷
*/
#define LRU_CLOCK() ((1000/server.hz <= LRU_CLOCK_RESOLUTION) ? server.lruclock : getLRUClock())
unsigned int getLRUClock(void) {
return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}
計算 LRU 時間方法如下:
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
// 處理 LRU 時間溢位的情況
return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
LRU_CLOCK_RESOLUTION;
}
}
當LRU_CLOCK_RESOLUTION
為 1000ms 時,robj.lru
最長可記錄的 LRU 時長為 194 天0xFFFFFF / 3600 / 24
。
更新 LFU 計數
當時使用 LFU 演算法時,robj.lru
被分為兩部分:16bits 記錄最近一次訪問時間,8bits 用作計數器
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val); // 衰減計數
counter = LFULogIncr(counter); // 增加計數
val->lru = (LFUGetTimeInMinutes()<<8) | counter; // 更新時間
}
更新訪問時間
前 16bits 用於儲存最近一次被訪問的時間:
/**
* 獲取 UNIX 分鐘時間戳,且只保留最低 16bits
* 用於表示最近一次衰減時間 LDT (last decrement time)
*/
unsigned long LFUGetTimeInMinutes(void) {
return (server.unixtime/60) & 65535;
}
增加訪問計數
後 8bits 是一個對數計數器 logarithmic counter
,裡面儲存的是訪問次數的對數:
#define LFU_INIT_VAL 5
// 對數遞增計數器,最大值為 255
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
return counter;
}
當 server.lfu_log_factor = 10
時,p = 1/((counter-LFU_INIT_VAL)*server.lfu_log_factor+1)
的增長函式如圖所示:
使用函式 rand()
生成的介於 0 與 1 之間隨機浮點數 r
符合均勻分佈,隨著 counter
的增大,其自增成功的概率迅速降低。
下列表格展示了 counter
在不同 lfu_log_factor
情況下,達到飽和(255)所需的訪問次數:
+--------+------------+------------+------------+------------+------------+
| factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
+--------+------------+------------+------------+------------+------------+
| 0 | 104 | 255 | 255 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 1 | 18 | 49 | 255 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 10 | 10 | 18 | 142 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 100 | 8 | 11 | 49 | 143 | 255 |
+--------+------------+------------+------------+------------+------------+
衰減訪問計數
同樣的,為了保證過期的熱點資料能夠被及時淘汰,redis 使用如下衰減函式:
// 計算距離上一次衰減的時間 ,單位為分鐘
unsigned long LFUTimeElapsed(unsigned long ldt) {
unsigned long now = LFUGetTimeInMinutes();
if (now >= ldt) return now-ldt;
return 65535-ldt+now;
}
/**
* 衰減函式,返回根據 LDT 時間戳衰減後的 LFU 計數
* 不更新計數器
*/
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8;
unsigned long counter = o->lru & 255;
/**
* 衰減因子 server.lfu_decay_time 用於控制計數器的衰減速度
* 每過 server.lfu_decay_time 分鐘訪問計數減 1
* 預設值為 1
*/
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
16bits 最多能儲存的分鐘數,換算成天數約為 45 天,因此 LDT 時間戳每隔 45 天就會重置一次。
執行刪除
每當客戶端執行命令產生新資料時,redis 會檢查記憶體使用是否超過 maxmemory
,如果超過則嘗試根據 maxmemory_policy
淘汰資料:
// redis 處理命令的主方法,在真正執行命令前,會有各種檢查,包括對OOM情況下的處理:
int processCommand(client *c) {
// ...
// 設定了 maxmemory 時,如果有必要,嘗試釋放記憶體(evict)
if (server.maxmemory && !server.lua_timedout) {
int out_of_memory = (performEvictions() == EVICT_FAIL);
// ...
// 如果釋放記憶體失敗,並且當前將要執行的命令不允許OOM(一般是寫入類命令)
if (out_of_memory && reject_cmd_on_oom) {
rejectCommand(c, shared.oomerr); // 向客戶端返回OOM
return C_OK;
}
}
}
實際執行刪除的是 performEvictions
函式:
int performEvictions(void) {
// 迴圈,嘗試釋放足夠大的記憶體
while (mem_freed < (long long)mem_tofree) {
// ...
if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL)
{
/**
* redis 使用的是近似 LRU / LFU 演算法
* 在淘汰物件時不會遍歷所有記錄,而是對記錄進行取樣
* EvictionPoolLRU 被用於臨時儲存應該被優先淘汰的樣本資料
*/
struct evictionPoolEntry *pool = EvictionPoolLRU;
// 根據配置的 maxmemory-policy,拿到一個可以釋放掉的bestkey
while(bestkey == NULL) {
unsigned long total_keys = 0, keys;
// 遍歷所有的 db 例項
for (i = 0; i < server.dbnum; i++) {
db = server.db+i;
dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
db->dict : db->expires;
// 根據 policy 選擇取樣的集合(keyspace 或 expire set)
if ((keys = dictSize(dict)) != 0) {
// 取樣並填充 pool
evictionPoolPopulate(i, dict, db->dict, pool);
total_keys += keys;
}
}
// 遍歷 pool 中的記錄,釋放記憶體
for (k = EVPOOL_SIZE-1; k >= 0; k--) {
if (pool[k].key == NULL) continue;
bestdbid = pool[k].dbid;
if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
de = dictFind(server.db[pool[k].dbid].dict, pool[k].key);
} else {
de = dictFind(server.db[pool[k].dbid].expires, pool[k].key);
}
// 將記錄從 pool 中剔除
if (pool[k].key != pool[k].cached)
sdsfree(pool[k].key);
pool[k].key = NULL;
pool[k].idle = 0;
if (de) {
// 提取該記錄的 key
bestkey = dictGetKey(de);
break;
} else {
/* Ghost... Iterate again. */
}
}
}
}
// 最終選中了一個 bestkey
if (bestkey) {
// 如果配置了 lazyfree-lazy-eviction,嘗試非同步刪除
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
// ...
} else {
goto cant_free; /* nothing to free... */
}
}
}
負責取樣的 evictionPoolPopulate
函式:
#define EVPOOL_SIZE 16
#define EVPOOL_CACHED_SDS_SIZE 255
struct evictionPoolEntry {
unsigned long long idle; /* LRU 空閒時間 / LFU 頻率倒數(優先淘汰該值較大的記錄) */
sds key; /* 參與淘汰篩選的鍵 */
sds cached; /* 鍵名快取 */
int dbid; /* 資料庫ID */
};
// evictionPool 陣列用於輔助 eviction 操作
static struct evictionPoolEntry *evictionPoolEntry;
/**
* 在給定的 sampledict 集合中進行取樣
* 並將其中應該被淘汰的記錄記錄至 evictionPool
*/
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
int j, k, count;
dictEntry *samples[server.maxmemory_samples];
// 從 sampledict 中隨機獲取 maxmemory_samples 個樣本資料
count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
// 遍歷樣本資料
for (j = 0; j < count; j++) {
// 根據 maxmemory_policy 計算樣本空閒時間 idle
if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(o);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
idle = 255-LFUDecrAndReturn(o);
} else {
// ...
}
k = 0; // 根據 idle 定位樣本在 evictionPool 中的索引(樣本按照 idle 升序)
while (k < EVPOOL_SIZE && pool[k].key && pool[k].idle < idle) k++;
if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
// 樣本空閒時間不夠長,不參與該輪 eviction
continue;
} else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
// 樣本對應的位置為空,可以直接插入至該位置
} else {
// 樣本對應的位置已被佔用,移動其他元素空出該位置
}
// ...
// 將樣本資料插入其對應的位置 k
int klen = sdslen(key);
if (klen > EVPOOL_CACHED_SDS_SIZE) {
pool[k].key = sdsdup(key);
} else {
// 如果 key 長度不超過 EVPOOL_CACHED_SDS_SIZE,則複用 sds 物件
}
pool[k].idle = idle;
pool[k].dbid = dbid;
}
}
Java 實現
在瞭解以上知識後,嘗試使用 Java 實現 執行緒安全 的淘汰策略。
確定簿記結構
在一個多執行緒安全的快取中,很重要的一點是減少簿記:
- 一方面避免額外狀態的維護開銷
- 另一方面可以減少系統處於不一致狀態的邊界情況
因此參考 redis 使用計數器來記錄訪問模式:
/**
* 快取記錄
*/
public abstract class CacheEntry {
// CAS Updater
private static final AtomicLongFieldUpdater<CacheEntry>
TTL_UPDATER = AtomicLongFieldUpdater.newUpdater(CacheEntry.class, "ttl");
// 快取記錄的剩餘存活時間(無符號長整數)
private volatile long ttl;
protected CacheEntry(long ttl) {
this.ttl = ttl;
}
public long ttl() {
return ttl;
}
// 支援併發更新 TTL
public boolean casTTL(long old, long ttl) {
return TTL_UPDATER.compareAndSet(this, old, ttl);
}
}
/**
* 淘汰策略
*/
public interface EvictStrategy {
// 更新快取記錄的 TTL
void updateTTL(CacheEntry node);
// 根據當前時間戳,計算快取記錄的 TTL
long weightTTL(CacheEntry node, long now);
}
確定刪除策略
受限於簿記結構,redis 只能通過取樣來規避大量的遍歷,減少 實時刪除 策略對主執行緒的阻塞。
而在對於記憶體限制沒那麼嚴謹的情況下,可以使用 懶惰刪除 策略,減少單次請求的開銷:
public abstract class EvictableCache {
EvictStrategy evicting; // 淘汰策略
/**
* 在讀寫快取記錄時,更新該記錄的 TTL
* @param entry 最近被訪問的快取記錄
*/
void accessEntry(CacheEntry entry) {
evicting.updateTTL(entry);
}
/**
* 批量淘汰快取
* @param evictSamples 快取樣本
* @param evictNum 最大淘汰數量
* @return 應該被淘汰的記錄
*/
Collection<CacheEntry> evictEntries(Iterable<CacheEntry> evictSamples, int evictNum) {
// 比較兩個 CacheEntry 的 TTL(優先淘汰 TTL 較小的記錄)
Comparator<CacheEntry> comparator = new Comparator<CacheEntry>() {
final long now = System.currentTimeMillis();
public int compare(CacheEntry o1, CacheEntry o2) {
long w1 = evicting.weightTTL(o1, now);
long w2 = evicting.weightTTL(o2, now);
return -Long.compareUnsigned(w1, w2);
}
};
// 使用大頂堆記錄 TTL 最小的 K 個 CacheEntry
PriorityQueue<CacheEntry> evictPool = new PriorityQueue<>(evictNum, comparator);
Iterator<CacheEntry> iterator = evictSamples.iterator();
while (iterator.hasNext()) {
CacheEntry entry = iterator.next();
if (evictPool.size() < evictNum) {
evictPool.add(entry);
} else {
// 如果 CacheEntry 的 TTL 小於堆頂記錄
// 則彈出堆頂記錄,並將 TTL 更小的記錄放入堆中
CacheEntry top = evictPool.peek();
if (comparator.compare(entry, top) < 1) {
evictPool.poll();
evictPool.add(entry);
}
}
}
return evictPool;
}
}
實現淘汰策略
FIFO 策略
/**
* FIFO 策略
*/
public class FirstInFirstOut implements EvictStrategy {
// 計數器,每發生一次訪問操作自增 1
private final AtomicLong counter = new AtomicLong(0);
// 第一次訪問時才更新 TTL
public void updateTTL(CacheEntry node) {
node.casTTL(0, counter.incrementAndGet());
}
// 返回第一次被訪問的序號
public long weightTTL(CacheEntry node, long now) {
return node.ttl();
}
}
LRU 策略
/**
* LRU-2 策略
*/
public class LeastRecentlyUsed implements EvictStrategy {
// 邏輯時鐘,每發生一次訪問操作自增 1
private final AtomicLong clock = new AtomicLong(0);
/**
* 更新 LRU 時間
*/
public void updateTTL(CacheEntry node) {
long old = node.ttl();
long tick = clock.incrementAndGet();
long flag = old == 0 ? Long.MIN_VALUE: 0;
// flag = Long.MIN_VALUE 表示放入 History Queue
// flag = 0 表示放入 LRU Cache
long ttl = (tick & Long.MAX_VALUE) | flag;
while ((old & Long.MAX_VALUE) < tick && ! node.casTTL(old, ttl)) {
old = node.ttl();
ttl = tick & Long.MAX_VALUE; // CAS 失敗說明已經是二次訪問
}
}
/**
* 根據 LRU 時間計算 TTL
*/
public long weightTTL(CacheEntry node, long now) {
long ttl = node.ttl();
return -1L - ttl;
}
}
LFU 策略
/**
* LFU-AgeDecay 策略
*/
public class LeastFrequentlyUsed implements EvictStrategy {
private static final int TIMESTAMP_BITS = 40; // 40bits 記錄訪問時間戳(保證 34 年不溢位)
private static final int FREQUENCY_BITS = 24; // 24bits 作為對數計數器(可以忽略計數溢位的情況)
private final long ERA = System.currentTimeMillis(); // 起始時間(記錄相對於該值的時間戳)
private final double LOG_FACTOR = 1; // 對數因子
private final TimeUnit DECAY_UNIT = TimeUnit.MINUTES; // 時間衰減單位
/**
* 更新 LFU 計數器與訪問時間
* 與 redis 不同,更新時不會對計數進行衰減
*/
public void updateTTL(CacheEntry node) {
final long now = System.currentTimeMillis();
long old = node.ttl();
long timestamp = old >>> FREQUENCY_BITS;
long frequency = old & (~0L >>> TIMESTAMP_BITS);
// 計算訪問時間
long elapsed = Math.min(~0L >>> FREQUENCY_BITS, now - ERA);
while (timestamp < elapsed) {
// 增加訪問計數
double rand = ThreadLocalRandom.current().nextDouble();
if (1./(frequency * LOG_FACTOR + 1) > rand) {
frequency++;
frequency &= (~0L >>> TIMESTAMP_BITS);
}
// 更新 TTL
long ttl = elapsed << FREQUENCY_BITS | frequency & (~0L >>> TIMESTAMP_BITS);
if (node.casTTL(old, ttl)) {
break;
}
old = node.ttl();
timestamp = old >>> FREQUENCY_BITS;
frequency = old & (~0L >>> TIMESTAMP_BITS);
}
}
/**
* 返回衰減後的 LFU 計數
*/
public long weightTTL(CacheEntry node, long now) {
long ttl = node.ttl();
long timestamp = ttl >>> FREQUENCY_BITS;
long frequency = ttl & (~0L >>> TIMESTAMP_BITS);
long decay = DECAY_UNIT.toMinutes(Math.max(now - ERA, timestamp) - timestamp);
return frequency - decay;
}
}
至此,對 redis 的淘汰策略分析完畢,後續將對 redis 的一些其他細節進行分享,感謝觀看。