詳解 Redis 記憶體管理機制和實現

来源:https://www.cnblogs.com/remcarpediem/archive/2019/10/28/11755860.html
-Advertisement-
Play Games

Redis是一個基於記憶體的鍵值資料庫,其記憶體管理是非常重要的。本文記憶體管理的內容包括:過期鍵的懶性刪除和過期刪除以及記憶體溢出控制策略。 ...


Redis是一個基於記憶體的鍵值資料庫,其記憶體管理是非常重要的。本文記憶體管理的內容包括:過期鍵的懶性刪除和過期刪除以及記憶體溢出控制策略。

最大記憶體限制

Redis使用 maxmemory 參數限制最大可用記憶體,預設值為0,表示無限制。限制記憶體的目的主要 有:

  • 用於緩存場景,當超出記憶體上限 maxmemory 時使用 LRU 等刪除策略釋放空間。
  • 防止所用記憶體超過伺服器物理記憶體。因為 Redis 預設情況下是會儘可能多使用伺服器的記憶體,可能會出現伺服器記憶體不足,導致 Redis 進程被殺死

maxmemory 限制的是Redis實際使用的記憶體量,也就是 used_memory統計項對應的記憶體。由於記憶體碎片率的存在,實際消耗的記憶體 可能會比maxmemory設置的更大,實際使用時要小心這部分記憶體溢出。具體Redis 記憶體監控的內容請查看一文瞭解 Redis 記憶體監控和記憶體消耗

Redis預設無限使用伺服器記憶體,為防止極端情況下導致系統記憶體耗 盡,建議所有的Redis進程都要配置maxmemory。 在保證物理記憶體可用的情況下,系統中所有Redis實例可以調整 maxmemory參數來達到自由伸縮記憶體的目的。

記憶體回收策略

Redis 回收記憶體大致有兩個機制:一是刪除到達過期時間的鍵值對象;二是當記憶體達到 maxmemory 時觸發記憶體移除控制策略,強制刪除選擇出來的鍵值對象。

刪除過期鍵對象

Redis 所有的鍵都可以設置過期屬性,內部保存在過期表中,鍵值表和過期表的結果如下圖所示。當 Redis保存大量的鍵,對每個鍵都進行精準的過期刪除可能會導致消耗大量的 CPU,會阻塞 Redis 的主線程,拖累 Redis 的性能,因此 Redis 採用惰性刪除和定時任務刪除機制實現過期鍵的記憶體回收。

 

 

惰性刪除是指當客戶端操作帶有超時屬性的鍵時,會檢查是否超過鍵的過期時間,然後會同步或者非同步執行刪除操作並返回鍵已經過期。這樣可以節省 CPU成本考慮,不需要單獨維護過期時間鏈表來處理過期鍵的刪除。

過期鍵的惰性刪除策略由 db.c/expireifNeeded 函數實現,所有對資料庫的讀寫命令執行之前都會調用 expireifNeeded 來檢查命令執行的鍵是否過期。如果鍵過期,expireifNeeded 會將過期鍵從鍵值表和過期表中刪除,然後同步或者非同步釋放對應對象的空間。源碼展示的時 Redis 4.0 版本。

expireIfNeeded 先從過期表中獲取鍵對應的過期時間,如果當前時間已經超過了過期時間(lua腳本執行則有特殊邏輯,詳看代碼註釋),則進入刪除鍵流程。刪除鍵流程主要進行了三件事:

  • 一是刪除操作命令傳播,通知 slave 實例並存儲到 AOF 緩衝區中
  • 二是記錄鍵空間事件,
  • 三是根據 lazyfreelazyexpire 是否開啟進行非同步刪除或者非同步刪除操作。
 1 int expireIfNeeded(redisDb *db, robj *key) {
 2     // 獲取鍵的過期時間
 3     mstime_t when = getExpire(db,key);
 4     mstime_t now;
 5     // 鍵沒有過期時間
 6     if (when < 0) return 0;
 7     // 實例正在從硬碟 laod 數據,比如說 RDB 或者 AOF
 8     if (server.loading) return 0;
 9 
10     // 當執行lua腳本時,只有鍵在lua一開始執行時
11     // 就到了過期時間才算過期,否則在lua執行過程中不算失效
12     now = server.lua_caller ? server.lua_time_start : mstime();
13 
14     // 當本實例是slave時,過期鍵的刪除由master發送過來的
15     // del 指令控制。但是這個函數還是將正確的信息返回給調用者。
16     if (server.masterhost != NULL) return now > when;
17     // 判斷是否未過期
18     if (now <= when) return 0;
19 
20     // 代碼到這裡,說明鍵已經過期,而且需要被刪除
21     server.stat_expiredkeys++;
22     // 命令傳播,到 slave 和 AOF
23     propagateExpire(db,key,server.lazyfree_lazy_expire);
24     // 鍵空間通知使得客戶端可以通過訂閱頻道或模式, 來接收那些以某種方式改動了 Redis 數據集的事件。
25     notifyKeyspaceEvent(NOTIFY_EXPIRED,
26         "expired",key,db->id);
27     // 如果是惰性刪除,調用dbAsyncDelete,否則調用 dbSyncDelete
28     return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
29                                          dbSyncDelete(db,key);
30 }

 

 

上圖是寫命令傳播的示意圖,刪除命令的傳播和它一致。propagateExpire 函數先調用 feedAppendOnlyFile 函數將命令同步到 AOF 的緩衝區中,然後調用 replicationFeedSlaves函數將命令同步到所有的 slave 中。Redis 複製的機制可以查看Redis 複製過程詳解

// 將命令傳遞到slave和AOF緩衝區。maser刪除一個過期鍵時會發送Del命令到所有的slave和AOF緩衝區
void propagateExpire(redisDb *db, robj *key, int lazy) {
    robj *argv[2];
    // 生成同步的數據
    argv[0] = lazy ? shared.unlink : shared.del;
    argv[1] = key;
    incrRefCount(argv[0]);
    incrRefCount(argv[1]);
    // 如果開啟了 AOF 則追加到 AOF 緩衝區中
    if (server.aof_state != AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    // 同步到所有 slave
    replicationFeedSlaves(server.slaves,db->id,argv,2);

    decrRefCount(argv[0]);
    decrRefCount(argv[1]);
}

 

dbAsyncDelete 函數會先調用 dictDelete 來刪除過期表中的鍵,然後處理鍵值表中的鍵值對象。它會根據值的占用的空間來選擇是直接釋放值對象,還是交給 bio 非同步釋放值對象。判斷依據就是值的估計大小是否大於 LAZYFREE_THRESHOLD 閾值。鍵對象和 dictEntry 對象則都是直接被釋放。

 

 

#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
    // 刪除該鍵在過期表中對應的entry
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    // unlink 該鍵在鍵值表對應的entry
    dictEntry *de = dictUnlink(db->dict,key->ptr);
    // 如果該鍵值占用空間非常小,懶刪除反而效率低。所以只有在一定條件下,才會非同步刪除
    if (de) {
        robj *val = dictGetVal(de);
        size_t free_effort = lazyfreeGetFreeEffort(val);
        // 如果釋放這個對象消耗很多,並且值未被共用(refcount == 1)則將其加入到懶刪除列表
        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
            atomicIncr(lazyfree_objects,1);
            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
            dictSetVal(db->dict,de,NULL);
        }
    }

    // 釋放鍵值對,或者只釋放key,而將val設置為NULL來後續懶刪除
    if (de) {
        dictFreeUnlinkedEntry(db->dict,de);
        // slot 和 key 的映射關係是用於快速定位某個key在哪個 slot中。
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

 

dictUnlink 會將鍵值從鍵值表中刪除,但是卻不釋放 key、val和對應的表entry對象,而是將其直接返回,然後再調用dictFreeUnlinkedEntry進行釋放。dictDelete 是它的兄弟函數,但是會直接釋放相應的對象。二者底層都通過調用 dictGenericDelete來實現。dbAsyncDelete d的兄弟函數 dbSyncDelete 就是直接調用dictDelete來刪除過期鍵。

void dictFreeUnlinkedEntry(dict *d, dictEntry *he) {
    if (he == NULL) return;
    // 釋放key對象
    dictFreeKey(d, he);
    // 釋放值對象,如果它不為null
    dictFreeVal(d, he);
    // 釋放 dictEntry 對象
    zfree(he);
}

 

Redis 有自己的 bio 機制,主要是處理 AOF 落盤、懶刪除邏輯和關閉大文件fd。bioCreateBackgroundJob 函數將釋放值對象的 job 加入到隊列中,bioProcessBackgroundJobs會從隊列中取出任務,根據類型進行對應的操作。

 

void *bioProcessBackgroundJobs(void *arg) {
    .....
    while(1) {
        listNode *ln;

        ln = listFirst(bio_jobs[type]);
        job = ln->value;
        if (type == BIO_CLOSE_FILE) {
            close((long)job->arg1);
        } else if (type == BIO_AOF_FSYNC) {
            aof_fsync((long)job->arg1);
        } else if (type == BIO_LAZY_FREE) {
            // 根據參數來決定要做什麼。有參數1則要釋放它,有參數2和3是釋放兩個鍵值表
            // 過期表,也就是釋放db 只有參數三是釋放跳錶
            if (job->arg1)
                lazyfreeFreeObjectFromBioThread(job->arg1);
            else if (job->arg2 && job->arg3)
                lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);
            else if (job->arg3)
                lazyfreeFreeSlotsMapFromBioThread(job->arg3);
        }
        zfree(job);
        ......
    }
}

 

dbSyncDelete 則是直接刪除過期鍵,並且將鍵、值和 DictEntry 對象都釋放。

int dbSyncDelete(redisDb *db, robj *key) {
    // 刪除過期表中的entry
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
    // 刪除鍵值表中的entry
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        // 如果開啟了集群,則刪除slot 和 key 映射表中key記錄。
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        return 0;
    }
}

 

但是單獨用這種方式存在記憶體泄露的問題,當過期鍵一直沒有訪問將無法得到及時刪除,從而導致記憶體不能及時釋放。正因為如此,Redis還提供另一種定時任 務刪除機製作為惰性刪除的補充。

Redis 內部維護一個定時任務,預設每秒運行10次(通過配置控制)。定時任務中刪除過期鍵邏輯採用了自適應演算法,根據鍵的 過期比例、使用快慢兩種速率模式回收鍵,流程如下圖所示。

 

 

  • 1)定時任務首先根據快慢模式( 慢模型掃描的鍵的數量以及可以執行時間都比快模式要多 )和相關閾值配置計算計算本周期最大執行時間、要檢查的資料庫數量以及每個資料庫掃描的鍵數量。
  • 2) 從上次定時任務未掃描的資料庫開始,依次遍歷各個資料庫。
  • 3)從資料庫中隨機選手 ACTIVEEXPIRECYCLELOOKUPSPER_LOOP 個鍵,如果發現是過期鍵,則調用 activeExpireCycleTryExpire 函數刪除它。
  • 4)如果執行時間超過了設定的最大執行時間,則退出,並設置下一次使用慢模式執行。
  • 5)未超時的話,則判斷是否採樣的鍵中是否有25%的鍵是過期的,如果是則繼續掃描當前資料庫,跳到第3步。否則開始掃描下一個資料庫。

定期刪除策略由 expire.c/activeExpireCycle 函數實現。在redis事件驅動的迴圈中的eventLoop->beforesleep和 周期性操作 databasesCron 都會調用 activeExpireCycle 來處理過期鍵。但是二者傳入的 type 值不同,一個是ACTIVEEXPIRECYCLESLOW 另外一個是ACTIVEEXPIRECYCLEFAST。activeExpireCycle 在規定的時間,分多次遍歷各個資料庫,從 expires 字典中隨機檢查一部分過期鍵的過期時間,刪除其中的過期鍵,相關源碼如下所示。

void activeExpireCycle(int type) {
    // 上次檢查的db
    static unsigned int current_db = 0; 
    // 上次檢查的最大執行時間
    static int timelimit_exit = 0;
    // 上一次快速模式運行時間
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    // 每次檢查周期要遍歷的DB數
    int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime(), timelimit, elapsed;

    ..... // 一些狀態時不進行檢查,直接返回

    // 如果上次周期因為執行達到了最大執行時間而退出,則本次遍歷所有db,否則遍歷db數等於 CRON_DBS_PER_CALL
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    // 根據ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC計算本次最大執行時間
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    // 如果是快速模式,則最大執行時間為ACTIVE_EXPIRE_CYCLE_FAST_DURATION
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
    // 採樣記錄
    long total_sampled = 0;
    long total_expired = 0;
    // 依次遍歷 dbs_per_call 個 db
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);
        // 將db數增加,一遍下一次繼續從這個db開始遍歷
        current_db++;

        do {
            ..... // 申明變數和一些情況下 break
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
            // 主要迴圈,在過期表中進行隨機採樣,判斷是否比率大於25%
            while (num--) {
                dictEntry *de;
                long long ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                // 刪除過期鍵
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl > 0) {
                    /* We want the average TTL of keys yet not expired. */
                    ttl_sum += ttl;
                    ttl_samples++;
                }
                total_sampled++;
            }
            // 記錄過期總數
            total_expired += expired;
            // 即使有很多鍵要過期,也不阻塞很久,如果執行超過了最大執行時間,則返回
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            // 當比率小於25%時返回
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
    .....// 更新一些server的記錄數據
}

 

activeExpireCycleTryExpire 函數的實現就和 expireIfNeeded 類似,這裡就不贅述了。

int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
    long long t = dictGetSignedIntegerVal(de);
    if (now > t) {
        sds key = dictGetKey(de);
        robj *keyobj = createStringObject(key,sdslen(key));

        propagateExpire(db,keyobj,server.lazyfree_lazy_expire);
        if (server.lazyfree_lazy_expire)
            dbAsyncDelete(db,keyobj);
        else
            dbSyncDelete(db,keyobj);
        notifyKeyspaceEvent(NOTIFY_EXPIRED,
            "expired",keyobj,db->id);
        decrRefCount(keyobj);
        server.stat_expiredkeys++;
        return 1;
    } else {
        return 0;
    }
}

 

定期刪除策略的關鍵點就是刪除操作執行的時長和頻率:

  • 如果刪除操作太過頻繁或者執行時間太長,就對 CPU 時間不是很友好,CPU 時間過多的消耗在刪除過期鍵上。
  • 如果刪除操作執行太少或者執行時間太短,就不能及時刪除過期鍵,導致記憶體浪費。

記憶體溢出控制策略

當Redis所用記憶體達到maxmemory上限時會觸發相應的溢出控制策略。 具體策略受maxmemory-policy參數控制,Redis支持6種策略,如下所示:

  • 1)noeviction:預設策略,不會刪除任何數據,拒絕所有寫入操作並返 回客戶端錯誤信息(error)OOM command not allowed when used memory,此 時Redis只響應讀操作。
  • 2)volatile-lru:根據LRU演算法刪除設置了超時屬性(expire)的鍵,直 到騰出足夠空間為止。如果沒有可刪除的鍵對象,回退到noeviction策略。
  • 3)allkeys-lru:根據LRU演算法刪除鍵,不管數據有沒有設置超時屬性, 直到騰出足夠空間為止。
  • 4)allkeys-random:隨機刪除所有鍵,直到騰出足夠空間為止。
  • 5)volatile-random:隨機刪除過期鍵,直到騰出足夠空間為止。
  • 6)volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過期數據。如果沒有,回退到noeviction策略。

記憶體溢出控制策略可以使用 config set maxmemory-policy {policy} 語句進行動態配置。Redis 提供了豐富的空間溢出控制策略,我們可以根據自身業務需要進行選擇。

當設置 volatile-lru 策略時,保證具有過期屬性的鍵可以根據 LRU 剔除,而未設置超時的鍵可以永久保留。還可以採用allkeys-lru 策略把 Redis 變為純緩存伺服器使用。

當Redis因為記憶體溢出刪除鍵時,可以通過執行 info stats 命令查看 evicted_keys 指標找出當前 Redis 伺服器已剔除的鍵數量。

每次Redis執行命令時如果設置了maxmemory參數,都會嘗試執行回收 記憶體操作。當Redis一直工作在記憶體溢出(used_memory>maxmemory)的狀態下且設置非 noeviction 策略時,會頻繁地觸發回收記憶體的操作,影響Redis 伺服器的性能,這一點千萬要引起註意。

個人博客,歡迎來玩


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Linux本地內核提權漏洞復現(CVE-2019-13272) 一、漏洞描述 當調用PTRACE_TRACEME時,ptrace_link函數將獲得對父進程憑據的RCU引用,然後將該指針指向get_cred函數。但是,對象struct cred的生存周期規則不允許無條件地將RCU引用轉換為穩定引用。 ...
  • 伺服器類型:linux =》centos 7.X以上版本 常用命令使用紫色加粗標明 1.安裝yum-util(為配置docker安裝時使用阿裡鏡像做準備):yum install -y yum-utils device-mapper-persistent-data lvm2 2.配置docker安裝 ...
  • 在httprunner運行報錯問題 代碼書寫: 當此段yml文件運行時會出現一下錯誤 在出現此錯誤時,為base_url的格式出現錯誤,只需修改base_url的位置 修改位置後運行yml文件 運行成功 ...
  • vmware workstation 虛擬機官方下載路徑:https://www.vmware.com/cn/products/workstation pro/workstation pro evaluation.html CentOS 官方下載路徑:https://www.centos.org/d ...
  • 回到目錄 在共集組態中,“輸入埠”和“輸出埠”共用BJT的集電極端子(故稱為“共集”),形成一個雙埠網路,如下圖所示: 圖3-7.01 由於共集組態的電路接法和共射組態的電路接法類似,只是輸出端從發射極取出而已,所以共射組態電路的各種輸入輸出特性和分析結果可以直接用到共集組態上,這裡就不再贅述 ...
  • [oot@cent65 bin]# service mysqld startStarting MySQL.2019-10-28T15:56:47.786960Z mysqld_safe error: log-error set to '/data/log/mysqld.log', however f ...
  • [TOC] 資料庫是什麼 資料庫:存儲數據的倉庫 為什麼使用資料庫 之前用excel來進行管理數據,有諸多問題: 電子錶格只能處理有限的數據列和數據行,對於數百萬玩、數千萬等巨大的數據列很難有效地處理 電子錶格無法提供安全、方便地許可權管理和控制手段 電子錶格很難實現多個數據之間地關聯 電子錶格很難實 ...
  • 為了方便以後查看,總結了一些常用的java操作hbase的代碼: ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...