在 Redis 中,允許用戶設置最大使用內存大小 server.maxmemory,在內存限定的情況下是很有用的。譬如,在一臺 8G 機子上部署了 4 個 Redis 服務點,每一個服務點分配 1G 的內存大小,減少內存緊張的情況,由此獲取更為穩(wěn)健的服務。Redis 內存數據集大小上升到一定大小的時候,就會施行數據淘汰策略。Redis 提供 6 種數據淘汰策略:
Redis 確定驅逐某個鍵值對后,會刪除這個數據,并將這個數據變更消息發(fā)布到本地(AOF 持久化)和從機(主從連接)。
在服務器配置中保存了 lru 計數器 server.lrulock,會定時(Redis 定時程序serverCorn())更新,server.lrulock 的值是根據 server.unixtime 計算出來的。
// redisServer 保存了lru 計數器
struct redisServer {
...
unsigned lruclock:22; /* Clock incrementing every minute, for LRU */
...
};
另外,從 struct redisObject 中可以發(fā)現,每一個 Redis 對象都會設置相應的 lru,即最近訪問的時間??梢韵胂蟮氖?,每一次訪問數據的時候,會更新 redisObject.lru。
LRU 數據淘汰機制是這樣的:在數據集中隨機挑選幾個鍵值對,取出其中 lru 最大的鍵值對淘汰。所以,你會發(fā)現,Redis 并不是保證取得所有數據集中最近最少使用(LRU)的鍵值對,而只是隨機挑選的幾個鍵值對中的。
// 每一個redis 對象都保存了lru
#define REDIS_LRU_CLOCK_MAX ((1<<21)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 10 /* LRU clock resolution in seconds */
typedef struct redisObject {
// 剛剛好32 bits
// 對象的類型,字符串/列表/集合/哈希表
unsigned type:4;
// 未使用的兩個位
unsigned notused:2; /* Not used */
// 編碼的方式,redis 為了節(jié)省空間,提供多種方式來保存一個數據
// 譬如:“123456789” 會被存儲為整數123456789
unsigned encoding:4;
unsigned lru:22; /* lru time (relative to server.lruclock) */
// 引用數
int refcount;
// 數據指針
void *ptr;
} robj;
// redis 定時執(zhí)行程序。聯(lián)想:linux cron
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
......
/* We have just 22 bits per object for LRU information.
* So we use an (eventually wrapping) LRU clock with 10 seconds resolution.
* 2^22 bits with 10 seconds resolution is more or less 1.5 years.
**
Note that even if this will wrap after 1.5 years it's not a problem,
* everything will still work but just some object will appear younger
* to Redis. But for this to happen a given object should never be touched
* for 1.5 years.
**
Note that you can change the resolution altering the
* REDIS_LRU_CLOCK_RESOLUTION define.
*/
updateLRUClock();
......
}
// 更新服務器的lru 計數器
void updateLRUClock(void) {
server.lruclock = (server.unixtime/REDIS_LRU_CLOCK_RESOLUTION) &
REDIS_LRU_CLOCK_MAX;
}
Redis 數據集數據結構中保存了鍵值對過期時間的表,即 redisDb.expires,在使用 SET 命令的時候,就有一個鍵值對超時時間的選項。和 LRU 數據淘汰機制類似,TTL 數據淘汰機制是這樣的:從過期時間 redisDB.expires 表中隨機挑選幾個鍵值對,取出其中 ttl 最大的鍵值對淘汰。同樣你會發(fā)現,Redis 并不是保證取得所有過期時間的表中最快過期的鍵值對,而只是隨機挑選的幾個鍵值對中的。
無論是什么機制,都是從所有的鍵值對中挑選合適的淘汰。
Redis 每服務客戶端執(zhí)行一個命令的時候,會檢測使用的內存是否超額。如果超額,即進行數據淘汰。
// 執(zhí)行命令
int processCommand(redisClient *c) {
......
// 內存超額
/* Handle the maxmemory directive.
**
First we try to free some memory if possible (if there are volatile
* keys in the dataset). If there are not the only thing we can do
* is returning an error. */
if (server.maxmemory) {
int retval = freeMemoryIfNeeded();
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
flagTransaction(c);
addReply(c, shared.oomerr);
return REDIS_OK;
}
}
......
}
這是我們之前講述過的命令處理函數。在處理命令處理函數的過程,會涉及到內存使用量的檢測,如果檢測到內存使用超額,會觸發(fā)數據淘汰機制。我們來看看淘汰機制觸發(fā)的函數 freeMemoryIfNeeded() 里面發(fā)生了什么。
// 如果需要,是否一些內存
int freeMemoryIfNeeded(void) {
size_t mem_used, mem_tofree, mem_freed;
int slaves = listLength(server.slaves);
// redis 從機回復空間和AOF 內存大小不計算入redis 內存大小
// 關于已使用內存大小是如何統(tǒng)計的,我們會其他章節(jié)講解,這里先忽略這個細節(jié)
/* Remove the size of slaves output buffers and AOF buffer from the
* count of used memory. */
mem_used = zmalloc_used_memory();
// 從機回復空間大小
if (slaves) {
listIter li;
listNode *ln;
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = listNodeValue(ln);
unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
if (obuf_bytes > mem_used)
mem_used = 0;
else
mem_used -= obuf_bytes;
}
}
// server.aof_buf && server.aof_rewrite_buf_blocks
if (server.aof_state != REDIS_AOF_OFF) {
mem_used -= sdslen(server.aof_buf);
mem_used -= aofRewriteBufferSize();
}
// 內存是否超過設置大小
/* Check if we are over the memory limit. */
if (mem_used <= server.maxmemory) return REDIS_OK;
// redis 中可以設置內存超額策略
if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
return REDIS_ERR; /* We need to free memory, but policy forbids. */
/* Compute how much memory we need to free. */
mem_tofree = mem_used - server.maxmemory;
mem_freed = 0;
while (mem_freed < mem_tofree) {
int j, k, keys_freed = 0;
// 遍歷所有數據集
for (j = 0; j < server.dbnum; j++) {
long bestval = 0; /* just to prevent warning */
sds bestkey = NULL;
struct dictEntry *de;
redisDb *db = server.db+j;
dict *dict;
// 不同的策略,選擇的數據集不一樣
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{
dict = server.db[j].dict;
} else {
dict = server.db[j].expires;
}
// 數據集為空,繼續(xù)下一個數據集
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);
}
// LRU 策略:挑選最近最少使用的數據
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
// server.maxmemory_samples 為隨機挑選鍵值對次數
// 隨機挑選server.maxmemory_samples 個鍵值對,驅逐最近最少使用的數據
for (k = 0; k < server.maxmemory_samples; k++) {
sds thiskey;
long thisval;
robj *o;
// 隨機挑選鍵值對
de = dictGetRandomKey(dict);
// 獲取鍵
thiskey = dictGetKey(de);
/* When policy is volatile-lru we need an additional lookup
* to locate the real key, as dict is set to db->expires. */
if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
de = dictFind(db->dict, thiskey);
o = dictGetVal(de);
// 計算數據的空閑時間
thisval = estimateObjectIdleTime(o);
// 當前鍵值空閑時間更長,則記錄
/* Higher idle time is better candidate for deletion */
if (bestkey == NULL || thisval > bestval) {
bestkey = thiskey;
bestval = thisval;
}
}
}
// TTL 策略:挑選將要過期的數據
/* volatile-ttl */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
// server.maxmemory_samples 為隨機挑選鍵值對次數
// 隨機挑選server.maxmemory_samples 個鍵值對,驅逐最快要過期的數據
for (k = 0; k < server.maxmemory_samples; k++) {
sds thiskey;
long thisval;
de = dictGetRandomKey(dict);
thiskey = dictGetKey(de);
thisval = (long) dictGetVal(de);
/* Expire sooner (minor expire unix timestamp) is better
* candidate for deletion */
if (bestkey == NULL || thisval < bestval) {
bestkey = thiskey;
bestval = thisval;
}
}
}
// 刪除選定的鍵值對
/* Finally remove the selected key. */
if (bestkey) {
long long delta;
robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
// 發(fā)布數據更新消息,主要是AOF 持久化和從機
propagateExpire(db,keyobj);
// 注意, propagateExpire() 可能會導致內存的分配,
// propagateExpire() 提前執(zhí)行就是因為redis 只計算
// dbDelete() 釋放的內存大小。倘若同時計算dbDelete()
// 釋放的內存和propagateExpire() 分配空間的大小,與此
// 同時假設分配空間大于釋放空間,就有可能永遠退不出這個循環(huán)。
// 下面的代碼會同時計算dbDelete() 釋放的內存和propagateExpire() 分配空間的大小// propagateExpire(db,keyobj);
// delta = (long long) zmalloc_used_memory();
// dbDelete(db,keyobj);
// delta -= (long long) zmalloc_used_memory();
// mem_freed += delta;
/////////////////////////////////////////
/* We compute the amount of memory freed by dbDelete() alone.
* It is possible that actually the memory needed to propagate
* the DEL in AOF and replication link is greater than the one
* we are freeing removing the key, but we can't account for
* that otherwise we would never exit the loop.
**
AOF and Output buffer memory will be freed eventually so
* we only care about memory used by the key space. */
// 只計算dbDelete() 釋放內存的大小
delta = (long long) zmalloc_used_memory();
dbDelete(db,keyobj);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta;
server.stat_evictedkeys++;
// 將數據的刪除通知所有的訂閱客戶端
notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",
keyobj, db->id);
decrRefCount(keyobj);
keys_freed++;
// 將從機回復空間中的數據及時發(fā)送給從機
/* When the memory to free starts to be big enough, we may
* start spending so much time here that is impossible to
* deliver data to the slaves fast enough, so we force the
* transmission here inside the loop. */
if (slaves) flushSlavesOutputBuffers();
}
}
// 未能釋放空間,且此時redis 使用的內存大小依舊超額,失敗返回
if (!keys_freed) return REDIS_ERR; /* nothing to free... */
}
return REDIS_OK;
}