Redis記憶體回收:LRU演算法
Redis技術交流群 481804090
Redis:https://github.com/zwjlpeng/Redis_Deep_Read
Redis中採用兩種演算法進行記憶體回收,引用計數演算法以及LRU演算法,在作業系統記憶體管理一節中,我們都學習過LRU演算法(最近最久未使用演算法),那麼什麼是LRU演算法呢
LRU演算法作為記憶體管理的一種有效演算法,其含義是在記憶體有限的情況下,當記憶體容量不足時,為了保證程式的執行,這時就不得不淘汰記憶體中的一些物件,釋放這些物件佔用的空間,那麼選擇淘汰哪些物件呢?LRU演算法就提供了一種策略,告訴我們選擇最近一段時間內,最久未使用的物件將其淘汰,至於為什麼要選擇最久未使用的,可以想想,最近一段時間內使用的東西,我們是不是可能一會又要用到呢~,而很長一段時間內都沒有使用過的東西,也許永遠都不會再使用~
在作業系統中LRU演算法淘汰的不是記憶體中的物件,而是頁,當記憶體中資料不足時,通過LRU演算法,選擇一頁(一般是4KB)將其交換到虛擬記憶體區(Swap區)
LRU演算法演示
這張圖應該畫的還行吧,用的是www.draw.io,解釋如下,假設前提,只有三塊記憶體空間可以使用,每一塊記憶體空間只能存放一個物件,如A、B、C...
1、最開始時,記憶體空間是空的,因此依次進入A、B、C是沒有問題的
2、當加入D時,就出現了問題,記憶體空間不夠了,因此根據LRU演算法,記憶體空間中A待的時間最為久遠,選擇A,將其淘汰
3、當再次引用B時,記憶體空間中的B又處於活躍狀態,而C則變成了記憶體空間中,近段時間最久未使用的
4、當再次向記憶體空間加入E時,這時記憶體空間又不足了,選擇在記憶體空間中待的最久的C將其淘汰出記憶體,這時的記憶體空間存放的物件就是E->B->D
LRU演算法的整體思路就是這樣的
演算法實現應該採用怎樣的資料結構
佇列?那不就是FIFO演算法嘛~,LRU演算法最為精典的實現,就是HashMap+Double LinkedList,時間複雜度為O(1),具體可以參考相關程式碼
REDIS中LRU演算法的實際應用,在Redis 1.0中並未引入LRU演算法,只是簡單的使用引用計數法,去掉記憶體中不再引用的物件以及執行一個定時任務serverCron去掉記憶體中已經過期的物件佔用的記憶體空間,以下是Redis 1.0中CT任務的釋放記憶體中的部份程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//去掉一些過期的KEYS for (j
= 0; j < server.dbnum; j++) { redisDb
*db = server.db+j; int num
= dictSize(db->expires); //計算hash表中過期Key的數目 if (num)
{ time_t now
= time (NULL); //#define
REDIS_EXPIRELOOKUPS_PER_CRON 100 if (num
> REDIS_EXPIRELOOKUPS_PER_CRON) num
= REDIS_EXPIRELOOKUPS_PER_CRON; //迴圈100次,從過期Hash表中隨機挑選出100個Key,判斷Key是否過期,如果過期了,執行刪除操作 while (num--)
{ dictEntry
*de; time_t t; //隨機獲取Key值(db->expires裡面儲存的均是即將過期的Keys) if ((de
= dictGetRandomKey(db->expires)) == NULL) break ; t
= ( time_t )
dictGetEntryVal(de); if (now
> t) { //不僅要從存放過期keys的Hash表中刪除資料,還要從存放實際資料的Hash表中刪除資料 deleteKey(db,dictGetEntryKey(de)); } } } } |
如果沒有看過Redis 1.0原始碼,理解起來可能有些困難,但看看1.0原始碼中的這個結構體,估計有點資料結構基礎的人,都明白上面這幾行程式碼的意思了(註釋部份我也已經寫的很清楚了)~
1
2
3
4
5
|
typedef struct redisDb
{ dict
*dict; //用來存放實際Key->Value資料的位置 dict
*expires; //用於記錄Key的過期時間 int id; //表示選擇的是第幾個redis庫 }
redisDb; |
沒有查證是從什麼版本開始,Redis增加了LRU演算法,以下是分析Redis 2.9.11程式碼中的LRU演算法淘汰策略,在2.9.11版本中與LRU演算法相關的程式碼主要位於object.c以及redis.c兩個原始檔中, 再分析這兩個檔案關於LRU原始碼之前,讓我們先看一下,Redis 2.9.11版本中關於LRU演算法的配置,配置檔案在redis.conf檔案中,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#
maxmemory < bytes > #
MAXMEMORY POLICY: how Redis will select what to remove when maxmemory #
is reached. You can select among five behaviors: # #
volatile-lru -> remove the key with an expire set using an LRU algorithm #
allkeys-lru -> remove any key accordingly to the LRU algorithm #
volatile-random -> remove a random key with an expire set #
allkeys-random -> remove a random key, any key #
volatile-ttl -> remove the key with the nearest expire time (minor TTL) #
noeviction -> don't expire at all, just return an error on write operations # #
Note: with any of the above policies, Redis will return an error on write #
operations, when there are not suitable keys for eviction. # #
At the date of writing this commands are: set setnx setex append #
incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd #
sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby #
zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby #
getset mset msetnx exec sort # #
The default is: # #
maxmemory-policy noeviction #
LRU and minimal TTL algorithms are not precise algorithms but approximated #
algorithms (in order to save memory), so you can tune it for speed or #
accuracy. For default Redis will check five keys and pick the one that was #
used less recently, you can change the sample size using the following #
configuration directive. # #
The default of 5 produces good enough results. 10 Approximates very closely #
true LRU but costs a bit more CPU. 3 is very fast but not very accurate. # #
maxmemory-samples 5 |
從上面的配置中,可以看出,高版本的Redis中當記憶體達到極限時,記憶體淘汰策略主要採用了6種方式進行記憶體物件的釋放操作
1.volatile-lru:從設定了過期時間的資料集中,選擇最近最久未使用的資料釋放
2.allkeys-lru:從資料集中(包括設定過期時間以及未設定過期時間的資料集中),選擇最近最久未使用的資料釋放
3.volatile-random:從設定了過期時間的資料集中,隨機選擇一個資料進行釋放
4.allkeys-random:從資料集中(包括了設定過期時間以及未設定過期時間)隨機選擇一個資料進行入釋放
5.volatile-ttl:從設定了過期時間的資料集中,選擇馬上就要過期的資料進行釋放操作
6.noeviction:不刪除任意資料(但redis還會根據引用計數器進行釋放呦~),這時如果記憶體不夠時,會直接返回錯誤
預設的記憶體策略是noeviction,在Redis中LRU演算法是一個近似演算法,預設情況下,Redis隨機挑選5個鍵,並且從中選取一個最近最久未使用的key進行淘汰,在配置檔案中可以通過maxmemory-samples的值來設定redis需要檢查key的個數,但是栓查的越多,耗費的時間也就越久,但是結構越精確(也就是Redis從記憶體中淘汰的物件未使用的時間也就越久~),設定多少,綜合權衡吧~~~
在redis.h中宣告的redisObj定義的如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#define
REDIS_LRU_BITS 24 #define
REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */ #define
REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */ typedef struct redisObject
{<br> //存放的物件型別 unsigned
type:4; //內容編碼 unsigned
encoding:4; //與server.lruclock的時間差值 unsigned
lru:REDIS_LRU_BITS; /*
lru time (relative to server.lruclock) */ \ //引用計數演算法使用的引用計數器 int refcount; //資料指標 void *ptr; }
robj; |
從redisObject結構體的定義中可以看出,在Redis中存放的物件不僅會有一個引用計數器,還會存在一個server.lruclock,這個變數會在定時器中每次重新整理時,呼叫getLRUClock獲取當前系統的毫秒數,作為LRU時鐘數,該計數器總共佔用24位,最大可以表示的值為24個1即((1<<REDIS_LRU_BITS) - 1)=2^24 - 1,單位是毫秒,你可以算一下這麼多毫秒,可以表示多少年~~
server.lruclock在redis.c中執行的定時器中進行更新操作,程式碼如下(redis.c中的定時器被配置中100ms執行一次)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int serverCron( struct aeEventLoop
*eventLoop, long long id, void *clientData)
{ ..... run_with_period(100)
trackOperationsPerSecond(); /*
We have just REDIS_LRU_BITS bits per object for LRU information. *
So we use an (eventually wrapping) LRU clock. * *
Note that even if the counter wraps it's not a big problem, *
everything will still work but some object will appear younger *
to Redis. However for this to happen a given object should never be *
touched for all the time needed to the counter to wrap, which is *
not likely. * *
Note that you can change the resolution altering the *
REDIS_LRU_CLOCK_RESOLUTION define. */ server.lruclock
= getLRUClock(); .... return 1000/server.hz; } |
看到這,再看看Redis中建立物件時,如何對redisObj中的unsigned lru進行賦值操作的,程式碼位於object.c中,如下所示
1
2
3
4
5
6
7
8
9
10
11
|
robj
*createObject( int type, void *ptr)
{ robj
*o = zmalloc( sizeof (*o)); o->type
= type; o->encoding
= REDIS_ENCODING_RAW; o->ptr
= ptr; o->refcount
= 1; //很關鍵的一步,Redis中建立的每一個物件,都記錄下該物件的LRU時鐘 /*
Set the LRU to the current lruclock (minutes resolution). */ o->lru
= LRU_CLOCK(); return o; } |
該程式碼中最為關鍵的一句就是o->lru=LRU_CLOCK(),這是一個定義,看一下這個巨集定義的實現,程式碼如下所示
1
|
#define
LRU_CLOCK() ((1000/server.hz <= REDIS_LRU_CLOCK_RESOLUTION) ? server.lruclock : getLRUClock()) |
其中REDIS_LRU_CLOCK_RESOLUTION為1000,可以自已在配置檔案中進行配置,表示的是LRU演算法的精度,在這裡我們就可以看到server.lruclock的用處了,如果定時器執行的頻率高於LRU演算法的精度時,可以直接將server.lruclock直接在物件建立時賦值過去,避免了函式呼叫的記憶體開銷以及時間開銷~
有了上述的基礎,下面就是最為關鍵的部份了,REDIS中LRU演算法,這裡以volatile-lru為例(選擇有過期時間的資料集進行淘汰),在Redis中命令的處理時,會呼叫processCommand函式,在ProcessCommand函式中,當在配置檔案中配置了maxmemory時,會呼叫freeMemoryIfNeeded函式,釋放不用的記憶體空間
以下是freeMemoryIfNeeded函式的關於LRU相關部份的原始碼,其他程式碼類似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
//不同的策略,操作的資料集不同 if (server.maxmemory_policy
== REDIS_MAXMEMORY_ALLKEYS_LRU || server.maxmemory_policy
== REDIS_MAXMEMORY_ALLKEYS_RANDOM) { dict
= server.db[j].dict; } else { //操作的是設定了過期時間的key集 dict
= server.db[j].expires; } if (dictSize(dict)
== 0) continue ; /*
volatile-random and allkeys-random policy */ //隨機選擇進行淘汰 if (server.maxmemory_policy
== REDIS_MAXMEMORY_ALLKEYS_RANDOM || server.maxmemory_policy
== REDIS_MAXMEMORY_VOLATILE_RANDOM) { de
= dictGetRandomKey(dict); bestkey
= dictGetKey(de); } /*
volatile-lru and allkeys-lru policy */ //具體的LRU演算法 else if (server.maxmemory_policy
== REDIS_MAXMEMORY_ALLKEYS_LRU || server.maxmemory_policy
== REDIS_MAXMEMORY_VOLATILE_LRU) { struct evictionPoolEntry
*pool = db->eviction_pool; while (bestkey
== NULL) { //選擇隨機樣式,並從樣本中作用LRU演算法選擇需要淘汰的資料 evictionPoolPopulate(dict,
db->dict, db->eviction_pool); /*
Go backward from best to worst element to evict. */ for (k
= REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) { if (pool[k].key
== NULL) continue ; de
= dictFind(dict,pool[k].key); sdsfree(pool[k].key); //將pool+k+1之後的元素向前平移一個單位 memmove (pool+k,pool+k+1, sizeof (pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1)); /*
Clear the element on the right which is empty *
since we shifted one position to the left. */ pool[REDIS_EVICTION_POOL_SIZE-1].key
= NULL; pool[REDIS_EVICTION_POOL_SIZE-1].idle
= 0; //選擇了需要淘汰的資料 if (de)
{ bestkey
= dictGetKey(de); break ; } else { /*
Ghost... */ continue ; } } } } |
看了上面的程式碼,也許你還在奇怪,說好的,LRU演算法去哪去了呢,再看看這個函式evictionPoolPopulate的實現吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
#define
EVICTION_SAMPLES_ARRAY_SIZE 16 void evictionPoolPopulate(dict
*sampledict, dict *keydict, struct evictionPoolEntry
*pool) { int j,
k, count; //EVICTION_SAMPLES_ARRAY_SIZE最大樣本數,預設16 dictEntry
*_samples[EVICTION_SAMPLES_ARRAY_SIZE]; dictEntry
**samples; //如果我們在配置檔案中配置的samples小於16,則直接使用EVICTION_SAMPLES_ARRAY_SIZE if (server.maxmemory_samples
<= EVICTION_SAMPLES_ARRAY_SIZE) { samples
= _samples; } else { samples
= zmalloc( sizeof (samples[0])*server.maxmemory_samples); } #if
1 /* Use bulk get by default. */ //從樣本集中隨機獲取server.maxmemory_samples個資料,存放在 count
= dictGetRandomKeys(sampledict,samples,server.maxmemory_samples); #else count
= server.maxmemory_samples; for (j
= 0; j < count; j++) samples[j] = dictGetRandomKey(sampledict); #endif for (j
= 0; j < count; j++) { unsigned long long idle; sds
key; robj
*o; dictEntry
*de; de
= samples[j]; key
= dictGetKey(de); if (sampledict
!= keydict) de = dictFind(keydict, key); o
= dictGetVal(de); //計算LRU時間 idle
= estimateObjectIdleTime(o); k
= 0; //選擇de在pool中的正確位置,按升序進行排序,升序的依據是其idle時間 while (k
< REDIS_EVICTION_POOL_SIZE && pool[k].key
&& pool[k].idle
< idle) k++; if (k
== 0 && pool[REDIS_EVICTION_POOL_SIZE-1].key != NULL) { /*
Can't insert if the element is < the worst element we have *
and there are no empty buckets. */ continue ; } else if (k
< REDIS_EVICTION_POOL_SIZE && pool[k].key == NULL) { /*
Inserting into empty position. No setup needed before insert. */ } else { //移動元素,memmove,還有空間可以插入新元素 if (pool[REDIS_EVICTION_POOL_SIZE-1].key
== NULL) { memmove (pool+k+1,pool+k, sizeof (pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1)); } else { //已經沒有空間插入新元素時,將第一個元素刪除 /*
No free space on right? Insert at k-1 */ k--; /*
Shift all elements on the left of k (included) to the *
left, so we discard the element with smaller idle time. */ //以下操作突出了第K個位置 sdsfree(pool[0].key); memmove (pool,pool+1, sizeof (pool[0])*k); } } //在第K個位置插入 pool[k].key
= sdsdup(key); pool[k].idle
= idle; } //執行到此之後,pool中存放的就是按idle
time升序排序 if (samples
!= _samples) zfree(samples); } |
看了上面的程式碼,LRU時鐘的計算並沒有包括在內,那麼在看一下LRU演算法的時鐘計算程式碼吧,LRU時鐘計算程式碼在object.c中的estimateObjectIdleTime這個函式中,程式碼如下~~
1
2
3
4
5
6
7
8
9
10
|
//精略估計LRU時間 unsigned long long estimateObjectIdleTime(robj
*o) { unsigned long long lruclock
= LRU_CLOCK(); if (lruclock
>= o->lru) { return (lruclock
- o->lru) * REDIS_LRU_CLOCK_RESOLUTION; } else { //這種情況一般不會發生,發生時證明redis中鍵的儲存時間已經wrap了 return (lruclock
+ (REDIS_LRU_CLOCK_MAX - o->lru)) * REDIS_LRU_CLOCK_RESOLUTION; } } |
好了,先到此吧~~~
轉載:http://www.cnblogs.com/WJ5888/p/4371647.html
相關文章
- redis的記憶體滿了之後,redis如何回收記憶體嗎Redis記憶體
- LRU工程實現原始碼(一):Redis 記憶體淘汰策略原始碼Redis記憶體
- Redis的記憶體回收機制和記憶體過期淘汰策略詳解Redis記憶體
- JVM記憶體分配策略,及垃圾回收演算法JVM記憶體演算法
- 記憶體回收介紹記憶體
- Redis記憶體——記憶體消耗(記憶體都去哪了?)Redis記憶體
- js記憶體回收機制JS記憶體
- .NET記憶體管理、垃圾回收記憶體
- java記憶體垃圾回收模型Java記憶體模型
- JVM記憶體回收機制——哪些記憶體需要被回收(JVM學習系列2)JVM記憶體
- Python 記憶體管理方式和垃圾回收演算法Python記憶體演算法
- JVM垃圾回收器、記憶體分配與回收策略JVM記憶體
- 【Redis記憶體策略】Redis記憶體
- Node - 記憶體管理和垃圾回收記憶體
- Node記憶體限制和垃圾回收記憶體
- Java記憶體管理 -JVM 垃圾回收Java記憶體JVM
- Java堆外直接記憶體回收Java記憶體
- JVM記憶體管理和垃圾回收JVM記憶體
- JavaScript 記憶體管理及垃圾回收JavaScript記憶體
- linux記憶體回收機制Linux記憶體
- Node記憶體限制與垃圾回收記憶體
- Java的記憶體回收機制Java記憶體
- jvm:記憶體模型、記憶體分配及GC垃圾回收機制JVM記憶體模型GC
- Python記憶體管理方式和垃圾回收演算法解析Python記憶體演算法
- golang 垃圾回收器如何標記記憶體?Golang記憶體
- 關於redis記憶體分析,記憶體優化Redis記憶體優化
- Java記憶體模型,垃圾回收機制,常用記憶體命令及工具Java記憶體模型
- JVM 之 記憶體分配與回收策略JVM記憶體
- PHP 垃圾回收與記憶體管理指引PHP記憶體
- JVM垃圾回收和記憶體分配策略JVM記憶體
- C# 垃圾回收釋放記憶體C#記憶體
- JVM的記憶體管理和垃圾回收JVM記憶體
- 圖解Java記憶體回收機制圖解Java記憶體
- 探索JVM的垃圾回收(堆記憶體)JVM記憶體
- 學習八、JavaScript的記憶體管理及垃圾回收(GC演算法)JavaScript記憶體GC演算法
- Redis效能篇(四)Redis記憶體碎片Redis記憶體
- Redis 記憶體壓縮原理Redis記憶體
- Redis的記憶體淘汰策略Redis記憶體