背景
鏈接:https://juejin.im/post/5da3dc4c518825647c513aa1
來源:掘金
為了減少占用內存空間,通常會對放到 redis 中的鍵通過 expire 設置一個過期時間,那 Redis 是怎么實現對過期鍵刪除的呢?
設置過期時間
設置過期時間的四種方式
# 將 key 的過期時間設置為 ttl 秒 expire <key> <ttl> # 將 key 的過期時間設置為 ttl 毫秒 pexpire <key> <ttl> # 將 key 的過期時間設置為 timestamp 指定的秒數時間戳 expire <key> <timestamp> # 將 key 的過期時間設置為 timestamp 指定的毫秒數時間戳 pexpire <key> <timestamp>復制代碼
其中前三種方式都會轉化為最后一種方式來實現過期時間
保存過期時間
我們看下 redisDb 的結構
typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ ... }復制代碼
可見在 redisDb 結構的 expire 字典(過期字典)保存了所有鍵的過期時間
過期字典的鍵是一個指向鍵空間中的某個鍵對象的指針
過期字典的值保存了鍵所指向的數據庫鍵的過期時間
注意
圖中過期字段和鍵空間中鍵對象有重復,實際中不會出現重復對象,鍵空間的鍵和過期字典的鍵都指向同一個鍵對象
過期鍵的判斷
通過查詢過期字典,檢查下面的條件判斷是否過期
- 檢查給定的鍵是否在過期字典中,如果存在就獲取鍵的過期時間
- 檢查當前 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 較重的工作,比如刷盤等操作。
上面源碼中根據是否配置 lazyfreelazyexpire(4.0版本引進) 來判斷是否執行惰性刪除,原理是先把過期對象進行邏輯刪除,然后在后臺進行真正的物理刪除,這樣就可以避免對象體積過大,造成阻塞,后面會在深入研究下 Redis 的 lazyfree 原理 源碼位置 lazyfree.c/dbAsyncDelete 方法
定期刪除
定期策略是每隔一段時間執行一次刪除過期鍵的操作,并通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU 時間的影響,同時也減少了內存浪費
Redis 默認會每秒進行 10 次(redis.conf 中通過 hz 配置)過期掃描,掃描并不是遍歷過期字典中的所有鍵,而是采用了如下方法
- 從過期字典中隨機取出 20 個鍵
- 刪除這 20 個鍵中過期的鍵
- 如果過期鍵的比例超過 25% ,重復步驟 1 和 2
為了保證掃描不會出現循環過度,導致線程卡死現象,還增加了掃描時間的上限,默認是 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 過期鍵刪除策略
Redis 服務器采用惰性刪除和定期刪除這兩種策略配合來實現,這樣可以平衡使用 CPU 時間和避免內存浪費
AOF、RDB 和復制功能對過期鍵的處理
RDB
生成 RDB 文件
在執行 save 命令或 bgsave 命令創建一個新的 RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵就不會被保存到新創建的 RDB文件中
載入 RDB 文件
主服務器:載入 RDB 文件時,會對鍵進行檢查,過期的鍵會被忽略
從服務器:載入 RDB文件時,所有鍵都會載入。但是會在主從同步的時候,清空從服務器的數據庫,所以過期的鍵載入也不會造成啥影響
AOF
AOF 文件寫入
當過期鍵被惰性刪除或定期刪除后,程序會向 AOF 文件追加一條 del 命令,來顯示的記錄該鍵已經被刪除
AOF 重寫
重啟過程會對鍵進行檢查,如果過期就不會被保存到重寫后的 AOF 文件中
復制
從服務器的過期鍵刪除動作由主服務器控制
主服務器在刪除一個過期鍵后,會顯示地向所有從服務器發送一個 del 命令,告知從服務器刪除這個過期鍵
從服務器收到在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將其刪除,只有在收到主服務器的 del 命令后,才會刪除,這樣就能保證主從服務器的數據一致性
疑問點?
- 如果主從服務器鏈接斷開怎么辦?
- 如果發生網絡抖動,主服務器發送的 del 命令沒有傳遞到從服務器怎么辦?
其實上面兩個問題 Redis 開發者已經考慮到了,只是主從復制涉及到的知識點還挺多,下面我就簡單的說下解決的思路,后面會再分享一篇主從復制的文件
首先看疑問點1-如果主從服務器鏈接斷開怎么辦?
Redis 采用 PSYNC 命令來執行復制時的同步操作,當從服務器在斷開后重新連接主服務器時,主服務器會把從服務器斷線期間執行的寫命令發送給從服務器,然后從服務器接收并執行這些寫命令,這樣主從服務器就會達到一致性,那主服務器如何判斷從服務器斷開鏈接的過程需要哪些命令?主服務器會維護一個固定長度的先進先出的隊列,即復制積壓緩沖區,緩沖區中保存著主服務器的寫命令和命令對應的偏移量,在主服務器給從服務器傳播命令時,同時也會往復制積壓緩沖區中寫命令。從服務器在向主服務器發送 PSYNC 命令時,同時會帶上它的最新寫命令的偏移量,這樣主服務器通過對比偏移量,就可以知道從服務器從哪里斷開的了
然后,我們再來看疑問點2-如果發生網絡抖動,主服務器發送的 del 命令沒有傳遞到從服務器怎么辦?
其實主從服務器之間會有心跳檢測機制,主從服務器通過發送和接收 REPLCONF ACK 命令來檢查兩者之間的網絡連接是否正常。當從服務器向主服務器發送 REPLCONF ACK 命令時,主服務器會對比自己的偏移量和從服務器發過來的偏移量,如果從服務器的偏移量小于自己的偏移量,主服務器會從復制積壓緩沖區中找到從服務器缺少的數據,并將數據發送給從服務器,這樣就達到了數據一致性
小結
本文主要分析了 Redis 的過期策略是采用惰性刪除和定期刪除兩種策略配合完成,然后簡單看了兩種策略的源碼和是怎么實現的。最后介紹了 Redis 在進行 RDB 、 AOF 和主從復制操作時,如何對過期鍵進行處理,特別介紹了主從復制在發生主從鏈接斷開和網絡抖動命令丟失是如何處理的,希望大家看完能有收獲