爲了減小佔用內存空間,一般會對放到 Redis 中的鍵經過 expire 設置一個過時時間,那 Redis 是怎麼實現對過時鍵刪除的呢?redis
# 將 key 的過時時間設置爲 ttl 秒 expire <key> <ttl> # 將 key 的過時時間設置爲 ttl 毫秒 pexpire <key> <ttl> # 將 key 的過時時間設置爲 timestamp 指定的秒數時間戳 expire <key> <timestamp> # 將 key 的過時時間設置爲 timestamp 指定的毫秒數時間戳 pexpire <key> <timestamp>
其中前三種方式都會轉化爲最後一種方式來實現過時時間shell
咱們看下 redisDb 的結構數據庫
typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ ... }
可見在 redisDb 結構的 expire 字典(過時字典)保存了全部鍵的過時時間服務器
過時字典的鍵是一個指向鍵空間中的某個鍵對象的指針網絡
過時字典的值保存了鍵所指向的數據庫鍵的過時時間dom
注意this
圖中過時字段和鍵空間中鍵對象有重複,實際中不會出現重複對象,鍵空間的鍵和過時字典的鍵都指向同一個鍵對象spa
經過查詢過時字典,檢查下面的條件判斷是否過時線程
檢查給定的鍵是否在過時字典中,若是存在就獲取鍵的過時時間設計
檢查當前 UNIX 時間戳是否大於鍵的過時時間,是就過時,不然未過時
在取出該鍵的時候對鍵進行過時檢查,即只對當前處理的鍵作刪除操做,不會在其餘過時鍵上花費 CPU 時間
**缺點:**對內存不友好,若是一哥鍵過時了,但會保存在內存中,若是這個鍵還不會被訪問,那麼久會形成內存浪費,甚至形成內存泄露
就是在執行 Redis 的讀寫命令前都會調用 expireIfNeeded 方法對鍵作過時檢查
若是鍵已通過期,expireIfNeeded 方法將其刪除
若是鍵未過時,expireIfNeeded 方法不作處理
對應源碼 db.c/expireIfNeeded 方法
int expireIfNeeded(redisDb *db, robj *key) { // 鍵未過時返回0 if (!keyIsExpired(db,key)) return 0; // 若是運行在從節點上,直接返回1,由於從節點不執行刪除操做,能夠看下面的複製部分 if (server.masterhost != NULL) return 1; // 運行到這裏,表示鍵帶有過時時間且運行在主節點上 // 刪除過時鍵個數 server.stat_expiredkeys++; // 向從節點和AOF文件傳播過時信息 propagateExpire(db,key,server.lazyfree_lazy_expire); // 發送事件通知 notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); // 根據配置(默認是同步刪除)判斷是否採用惰性刪除(這裏的惰性刪除是指採用後臺線程處理刪除操作,這樣會減小卡頓) return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); }
咱們一般說 Redis 是單線程的,其實 Redis 把處理網絡收發和執行命令的操做都放到了主線程,但 Redis 還有其餘後臺線程在工做,這些後臺線程通常從事 IO 較重的工做,好比刷盤等操做。
上面源碼中根據是否配置 lazyfree_lazy_expire(4.0版本引進) 來判斷是否執行惰性刪除,原理是先把過時對象進行邏輯刪除,而後在後臺進行真正的物理刪除,這樣就能夠避免對象體積過大,形成阻塞,後面會在深刻研究下 Redis 的 lazyfree 原理 源碼位置 lazyfree.c/dbAsyncDelete 方法
按期策略是每隔一段時間執行一次刪除過時鍵的操做,並經過限制刪除操做執行的時長和頻率來減小刪除操做對CPU 時間的影響,同時也減小了內存浪費
Redis 默認會每秒進行 10 次(redis.conf 中經過 hz 配置)過時掃描,掃描並非遍歷過時字典中的全部鍵,而是採用了以下方法
爲了保證掃描不會出現循環過分,致使線程卡死現象,還增長了掃描時間的上限,默認是 25 毫秒(即默認在慢模式下,若是是快模式,掃描上限是 1 毫秒)
對應源碼 expire.c/activeExpireCycle 方法
void activeExpireCycle(int type) { ... do { ... if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) // 選過時鍵的數量,爲 20 num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; while (num--) { dictEntry *de; long long ttl; // 隨機選 20 個過時鍵 if ((de = dictGetRandomKey(db->expires)) == NULL) break; ... // 嘗試刪除過時鍵 if (activeExpireCycleTryExpire(db,de,now)) expired++; ... } ... // 只有過時鍵比例 < 25% 才跳出循環 } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } ... }
由於 Redis 在掃描過時鍵時,通常會循環掃描屢次,若是請求進來,且正好服務器正在進行過時鍵掃描,那麼須要等待 25 毫秒,若是客戶端設置的超時時間小於 25 毫秒,那就會致使連接由於超時而關閉,就會形成異常,這些現象還不能從慢查詢日誌中查詢到,由於慢查詢只記錄邏輯處理過程,不包括等待時間。
因此咱們在設置過時時間時,必定要避免同時大批量鍵過時的現象,因此若是有這種狀況,最好給過時時間加個隨機範圍,緩解大量鍵同時過時,形成客戶端等待超時的現象
Redis 服務器採用惰性刪除和按期刪除這兩種策略配合來實現,這樣能夠平衡使用 CPU 時間和避免內存浪費
生成 RDB 文件
在執行 save 命令或 bgsave 命令建立一個新的 RDB文件時,程序會對數據庫中的鍵進行檢查,已過時的鍵就不會被保存到新建立的 RDB文件中
載入 RDB 文件
主服務器:載入 RDB 文件時,會對鍵進行檢查,過時的鍵會被忽略
從服務器:載入 RDB文件時,全部鍵都會載入。可是會在主從同步的時候,清空從服務器的數據庫,因此過時的鍵載入也不會形成啥影響
AOF 文件寫入
當過時鍵被惰性刪除或按期刪除後,程序會向 AOF 文件追加一條 del 命令,來顯示的記錄該鍵已經被刪除
AOF 重寫
重啓過程會對鍵進行檢查,若是過時就不會被保存到重寫後的 AOF 文件中
從服務器的過時鍵刪除動做由主服務器控制
主服務器在刪除一個過時鍵後,會顯示地向全部從服務器發送一個 del 命令,告知從服務器刪除這個過時鍵
從服務器收到在執行客戶端發送的讀命令時,即便碰到過時鍵也不會將其刪除,只有在收到主服務器的 del 命令後,纔會刪除,這樣就能保證主從服務器的數據一致性
其實上面兩個問題 Redis 開發者已經考慮到了,只是主從複製涉及到的知識點還挺多,下面我就簡單的說下解決的思路,後面會再分享一篇主從複製的文件
首先看疑問點1-若是主從服務器連接斷開怎麼辦?
Redis 採用 PSYNC 命令來執行復制時的同步操做,當從服務器在斷開後從新鏈接主服務器時,主服務器會把從服務器斷線期間執行的寫命令發送給從服務器,而後從服務器接收並執行這些寫命令,這樣主從服務器就會達到一致性,那主服務器如何判斷從服務器斷開連接的過程須要哪些命令?主服務器會維護一個固定長度的先進先出的隊列,即複製積壓緩衝區,緩衝區中保存着主服務器的寫命令和命令對應的偏移量,在主服務器給從服務器傳播命令時,同時也會往復制積壓緩衝區中寫命令。從服務器在向主服務器發送 PSYNC 命令時,同時會帶上它的最新寫命令的偏移量,這樣主服務器經過對比偏移量,就能夠知道從服務器從哪裏斷開的了
而後,咱們再來看疑問點2-若是發生網絡抖動,主服務器發送的 del 命令沒有傳遞到從服務器怎麼辦?
其實主從服務器之間會有心跳檢測機制,主從服務器經過發送和接收 REPLCONF ACK 命令來檢查二者之間的網絡鏈接是否正常。當從服務器向主服務器發送 REPLCONF ACK 命令時,主服務器會對比本身的偏移量和從服務器發過來的偏移量,若是從服務器的偏移量小於本身的偏移量,主服務器會從複製積壓緩衝區中找到從服務器缺乏的數據,並將數據發送給從服務器,這樣就達到了數據一致性
本文主要分析了 Redis 的過時策略是採用惰性刪除和按期刪除兩種策略配合完成,而後簡單看了兩種策略的源碼和是怎麼實現的。最後介紹了 Redis 在進行 RDB 、 AOF 和主從複製操做時,如何對過時鍵進行處理,特別介紹了主從複製在發生主從連接斷開和網絡抖動命令丟失是如何處理的,但願你們看完能有收穫
《Redis設計與實現》第二版.黃健宏
《Redis深度歷險:核心原理與深度實戰》.老錢
https://yq.aliyun.com/articles/205504 本文由博客一文多發平臺 OpenWrite 發佈!