源碼參考github redis/evict.c,commitid - d1a005ab3963c16b65e805675a76f0e40c723158
node
過程當中須要時刻提醒本身,閱讀源碼是爲了學習實現細節,但也不能陷入細節,分析順序按照執行順序,避免貼大塊源碼。git
當內存使用超過配置時的內存淘汰策略,很是好記憶:不處理/LRU算法/LFU算法/隨機/過時時間github
noeviction
: 不會驅逐任何keygolang
allkeys-lru
: 對全部key使用LRU算法進行刪除redis
volatile-lru
: 對全部設置了過時時間的key使用LRU算法進行刪除算法
allkeys-random:
對全部key隨機刪除緩存
volatile-random
: 對全部設置了過時時間的key隨機刪除數據結構
volatile-ttl
: 刪除立刻要過時的keydom
allkeys-lfu
: 對全部key使用LFU算法進行刪除函數
volatile-lfu
: 對全部設置了過時時間的key使用LFU算法進行刪除
沒有lua腳本處於超時狀態且不在載入數據狀態時,纔會執行freeMemoryIfNeeded()
函數
int freeMemoryIfNeededAndSafe(void) {
if (server.lua_timedout || server.loading) return C_OK;
return freeMemoryIfNeeded();
}
複製代碼
默認狀況下,從節點應該忽略maxmemory
指令,僅僅作從節點該作的事情就好
if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;
複製代碼
若是淘汰策略是noeviction
, 那redis只能說"我盡力了"
if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
goto cant_free; /* We need to free memory, but policy forbids. */
複製代碼
cant_free
有以下兩種狀況,它能作的只有檢查lazyfree
線程(應該是redis v4添加的)是否還有任務,而後等待。
noeviction
cant_free:
while(bioPendingJobsOfType(BIO_LAZY_FREE)) {
if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)
break;
usleep(1000);
}
return C_ERR;
}
複製代碼
經過getMaxmemoryState
函數(不重要,略),取得要釋放多少空間mem_tofrtee
,選擇策略開始清理
size_t mem_reported, mem_tofree, mem_freed;
if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)
return C_OK;
mem_freed = 0
while (mem_freed < mem_tofree) {
// 匹配的key
sds bestkey = NULL;
// bestkey所在的db
int bestdbid;
// LRU算法/LFU算法/TTL過時時間
if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) || server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
}
// 隨機淘汰
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)) {
}
// 刪除所選的key
if (bestkey) {
// 獲取當前內存
delta = (long long) zmalloc_used_memory();
// 是否非阻塞刪除
if (server.lazyfree_lazy_eviction)
dbAsyncDelete(db,keyobj);
else
dbSyncDelete(db,keyobj);
// 計算釋放了多少內存
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
} else {
goto cant_free; /* nothing to free... */
}
}
複製代碼
當選擇隨機淘汰時,會遍歷當前redis實例的每個db,若是是全部key隨機刪除選擇db->dict
, 不然只選擇設置了過時時間的集合: db->expires
, 若是每個db的dictSize(dict)
都是0,則會進入到上面提到的cant_free
的第二種狀況
for (i = 0; i < server.dbnum; i++) {
j = (++next_db) % server.dbnum;
db = server.db+j;
dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ? db->dict : db->expires;
if (dictSize(dict) != 0) {
de = dictGetRandomKey(dict);
bestkey = dictGetKey(de);
bestdbid = j;
break;
}
}
複製代碼
相關定義,能夠知道,淘汰策略的核心字段在於idle
這個屬性
#define EVPOOL_SIZE 16
#define EVPOOL_CACHED_SDS_SIZE 255
// 樣本集類型
struct evictionPoolEntry {
unsigned long long idle; /* Object idle time (inverse frequency for LFU) */
sds key; /* Key name. */
sds cached; /* Cached SDS object for key name. */
int dbid; /* Key DB number. */
};
static struct evictionPoolEntry *EvictionPoolLRU;
複製代碼
首先初始化樣本集,這部分和lru/lfu/ttl並沒有關係,核心在於evictionPoolPopulate
函數
// 初始化樣本集合
struct evictionPoolEntry *pool = EvictionPoolLRU;
while (bestkey == NULL) {
unsigned long total_keys = 0, keys;
// 遍歷每個db,更新/維護pool樣本集
for (i = 0; i < server.dbnum; i++) {
db = server.db+i;
dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ? db->dict : db->expires;
if ((keys = dictSize(dict)) != 0) {
// 四個參數能夠理解爲:dbid, 候選集合(db->dict/db->expires), 完整集合(db->dict), 樣本集合
evictionPoolPopulate(i, dict, db->dict, pool);
total_keys += keys;
}
}
// 無key可淘汰
if (!total_keys) break;
// 從後向前遍歷, 並重置樣本集
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[k].key = NULL;
pool[k].idle = 0;
// 尋找到bestkey,break
if (de) {
bestkey = dictGetKey(de);
break;
}
}
}
複製代碼
首先,根據maxmemory_samples
配置,選擇必定數量的樣本,這個值默認爲5,值越高越接近真實的LRU/LFU算法,值越低,性能越高,因此須要平衡
count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
for (j = 0; j < count; j++) {
// 見下一個代碼塊
}
複製代碼
遍歷,根據策略計算出每個樣本的idle
值,值越高,能夠理解爲匹配度越高,優先刪除
volatile-ttl
策略計算idle
值方式: 快過時的放後面lru
策略計算方式: 更久沒訪問的放前面lfu
策略計算方式方式: 訪問頻率更低的放在後面,頻率一致,比較誰更久沒訪問if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
idle = estimateObjectIdleTime(o);
} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
idle = 255-LFUDecrAndReturn(o);
} else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
idle = ULLONG_MAX - (long)dictGetVal(de);
} else {
serverPanic("Unknown eviction policy in evictionPoolPopulate()");
}
// 而後維護樣本集便可,後面的代碼省略了
複製代碼
Redis維護了一個24bit的全局時鐘,能夠簡單的理解爲當前系統的時間戳,每隔必定時間會更新這個時鐘,此處不拓展
// server.h 595行
#define LRU_BITS 24 // 24bit的時鐘
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) // obj->lru時鐘的最大值
#define LRU_CLOCK_RESOLUTION 1000 // 時鐘精度,毫秒
// server.h 1017行
stuct redisServer {
// Clock for LRU eviction
_Atomic unsigned int lruclock;
}
複製代碼
每一個key對象內部一樣維護了一個24bit的時鐘,後文中使用o->lru
指代
// server.h 600行
typedef struct redisObject {
// LRU time (relative to global lru_clock)
unsigned lru:LRU_BITS
}
複製代碼
設置全局lruclock,當lruclock的值超過LRU_CLOCK_MAX,會從頭開始計算
// src/evict.c 70行
unsigned int getLRUClock(void) {
return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}
複製代碼
設置o->lru
, 當新增key對象的時候會把系統的時鐘賦值到這個內部對象時鐘
// src/evict.c 78行
unsigned int LRU_CLOCK(void) {
unsigned int lruclock;
if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {
lruclock = server.lruclock;
} else {
lruclock = getLRUClock();
}
return lruclock;
}
複製代碼
好比如今要進行LRU,就須要對比lruclock
和o->lru
,它們有兩種狀況
lruclock >= o->lru
: 這是一般狀況lruclock < o->lru
: 當lruclock的值超過LRU_CLOCK_MAX,會從頭開始計算,所以會出現這種狀況,須要計算額外的時間// src/evict.c 88行
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru)) * LRU_CLOCK_RESOLUTION;
}
}
複製代碼
redis lfu的實現能夠用晦澀難懂來形容了,沒意思,不如去作leetcode
package problem0146
// 雙向鏈表結構
type LinkNode struct {
key, value int
pre, next *LinkNode
}
// LRU結構
type LRUCache struct {
m map[int]*LinkNode
cap int
head, tail *LinkNode
}
func Constructor(capacity int) LRUCache {
head := &LinkNode{0, 0, nil, nil}
tail := &LinkNode{0, 0, nil, nil}
head.next = tail
tail.pre = head
return LRUCache{make(map[int]*LinkNode, capacity), capacity, head, tail}
}
func (this *LRUCache) moveToHead(node *LinkNode) {
this.removeNode(node)
this.addNode(node)
}
func (this *LRUCache) removeNode(node *LinkNode) {
node.pre.next = node.next
node.next.pre = node.pre
}
func (this *LRUCache) addNode(node *LinkNode) {
head := this.head
node.next = head.next
node.next.pre = node
node.pre = head
head.next = node
}
// 若是有,將這個元素移動到首位置,返回值
// 若是沒有,返回-1
func (this *LRUCache) Get(key int) int {
if v, exist := this.m[key]; exist {
this.moveToHead(v)
return v.value
} else {
return -1
}
}
// 若是元素存在,將其移動到最前面,並更新值
// 若是元素不存在,插入到元素首,放入map(判斷容量)
func (this *LRUCache) Put(key int, value int) {
tail := this.tail
cache := this.m
if v, exist := cache[key]; exist {
v.value = value
this.moveToHead(v)
} else {
v := &LinkNode{key, value, nil, nil}
if len(this.m) == this.cap {
delete(cache, tail.pre.key)
this.removeNode(tail.pre)
}
this.addNode(v)
cache[key] = v
}
}
複製代碼
package problem0460
// LRU: Least Recently Used,緩存滿的時候,刪除緩存裏最久未使用的數據,而後放入新元素
// LFU: Least Frequently Used,緩存滿的時候,刪除緩存裏使用次數最少的元素,而後放入新元素,若是使用頻率同樣,刪除緩存最久的元素
// 節點:包含key, value, frequent訪問次數, pre前驅指針, next後繼指針
type Node struct {
key, value, frequent int
pre, next *Node
}
// 雙向鏈表:包含head頭指針, tail尾指針, size尺寸
type ListNode struct {
head, tail *Node
size int
}
// 雙向鏈表輔助函數:添加一個節點到頭節點後
func (this *ListNode) addNode(node *Node) {
head := this.head
node.next = head.next
node.next.pre = node
node.pre = head
head.next = node
}
// 雙向鏈表輔助函數,刪除一個節點
func (this *ListNode) removeNode(node *Node) {
node.pre.next = node.next
node.next.pre = node.pre
}
// LFUCache結構:包含capacity容量, size當前容量, minFrequent當前最少訪問頻次, cacheMap緩存哈希表, frequentMap頻次哈希表
// minFrequent當前最少訪問頻次:
// 1. 插入一個新節點時,以前確定沒訪問過,minFrequent = 1
// 2. put和get時,若是移除後雙向鏈表節點個數爲0,且剛好是最小訪問鏈表, minFrequent++
type LFUCache struct {
capacity, size, minFrequent int
cacheMap map[int]*Node
frequentMap map[int]*ListNode
}
func Constructor(capacity int) LFUCache {
return LFUCache{
capacity: capacity,
size: 0,
minFrequent: 0,
cacheMap: make(map[int]*Node),
frequentMap: make(map[int]*ListNode),
}
}
// LFUCache輔助函數:將節點從對應的頻次雙向鏈表中刪除
func (this *LFUCache) remove(node *Node) {
this.frequentMap[node.frequent].removeNode(node)
this.frequentMap[node.frequent].size--
}
// LFUCache輔助函數:將節點添加進對應的頻次雙向鏈表,沒有則建立
func (this *LFUCache) add(node *Node) {
if listNode, exist := this.frequentMap[node.frequent]; exist {
listNode.addNode(node)
listNode.size++
} else {
listNode = &ListNode{&Node{}, &Node{}, 0}
listNode.head.next = listNode.tail
listNode.tail.pre = listNode.head
listNode.addNode(node)
listNode.size++
this.frequentMap[node.frequent] = listNode
}
}
// LFUCache輔助函數:移除一個key
func (this *LFUCache) evictNode() {
listNode := this.frequentMap[this.minFrequent]
delete(this.cacheMap, listNode.tail.pre.key)
listNode.removeNode(listNode.tail.pre)
listNode.size--
}
// LFUCache輔助函數:獲取一個key和修改一個key都會增長對應key的訪問頻次,能夠獨立爲一個方法,完成以下任務:
// 1. 將對應node從頻次列表中移出
// 2. 維護minFrequent
// 3. 該節點訪問頻次++,移動進下一個訪問頻次鏈表
func (this *LFUCache) triggerVisit(node *Node) {
this.remove(node)
if node.frequent == this.minFrequent && this.frequentMap[node.frequent].size == 0 {
this.minFrequent++
}
node.frequent++
this.add(node)
}
func (this *LFUCache) Get(key int) int {
if node, exist := this.cacheMap[key]; exist {
this.triggerVisit(node)
return node.value
}
return -1
}
func (this *LFUCache) Put(key int, value int) {
if this.capacity == 0 {
return
}
if node, exist := this.cacheMap[key]; exist {
this.triggerVisit(node)
this.cacheMap[key].value = value
} else {
newNode := &Node{key, value, 1, nil, nil}
if this.size < this.capacity {
this.add(newNode)
this.size++
this.minFrequent = 1
} else {
this.evictNode()
this.add(newNode)
this.minFrequent = 1
}
this.cacheMap[key] = newNode
}
}
複製代碼