Redis淘汰策略
觸發時機
內存到達設定的閾值,再向redis追加數據,就會觸發淘汰策略 Redis是內存nosql庫,在實際的淘汰實現中,Redis 的淘汰算法是抽取一小部分(只限於設置了 expire 的部分)從中選出要淘汰的 鍵,從而減少內存消耗提升性能。
LRU算法
Redis使用的是近似LRU算法,它跟常規的LRU算法還不太一樣。近似LRU算法通過隨機採樣法淘汰數據,每次隨機出5(默認)個key,從裏面淘汰掉最近最少使用的key。
可以通過maxmemory-samples參數修改採樣數量: 例:maxmemory-samples 10 maxmenory-samples配置的越大,淘汰的結果越接近於嚴格的LRU算法
Redis爲了實現近似LRU算法,給每個key增加了一個額外增加了一個24bit的字段,用來存儲該key最後一次被訪問的時間。
Redis3.0對近似LRU算法進行了一些優化。新算法會維護一個候選池(大小爲16),池中的數據根據訪問時間進行排序,第一次隨機選取的key都會放入池中,隨後每次隨機選取的key只有在訪問時間小於池中最小的時間纔會放入池中,直到候選池被放滿。當放滿後,如果有新的key需要放入,則將池中最後訪問時間最大(最近被訪問)的移除。 當需要淘汰的時候,則直接從池中選取最近訪問時間最小(最久沒被訪問)的key淘汰掉就行。
LFU算法
LFU算法是Redis4.0裏面新加的一種淘汰策略。它的全稱是Least Frequently Used,它的核心思想是根據key的最近被訪問的頻率進行淘汰,很少被訪問的優先被淘汰,被訪問的多的則被留下來。
LFU算法能更好的表示一個key被訪問的熱度。假如你使用的是LRU算法,一個key很久沒有被訪問到,只剛剛是偶爾被訪問了一次,那麼它就被認爲是熱點數據,不會被淘汰,而有些key將來是很有可能被訪問到的則被淘汰了。如果使用LFU算法則不會出現這種情況,因爲使用一次並不會使一個key成爲熱點數據。
LFU把原來的key對象的內部時鐘的24位分成兩部分,前16位還代表時鐘,後8位代表一個計數器。16位的情況下如果還按照秒爲單位就會導致不夠用,所以一般這裏以時鐘爲單位。而後8位表示當前key對象的訪問頻率,8位只能代表255,但是redis並沒有採用線性上升的方式,而是通過一個複雜的公式,通過配置兩個參數來調整數據的遞增速度。 下圖從左到右表示key的命中次數,從上到下表示影響因子,在影響因子爲100的條件下,經過10M次命中才能把後8位值加滿到255.
lfu-log-factor 10
lfu-decay-time 1
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
return counter;
}
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8;
unsigned long counter = o->lru & 255;
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
上面說的情況是key一直被命中的情況,如果一個key經過幾分鐘沒有被命中,那麼後8位的值是需要遞減幾分鐘,具體遞減幾分鐘根據衰減因子lfu-decay-time來控制 上面遞增和衰減都有對應參數配置,那麼對於新分配的key呢?如果新分配的key計數器開始爲0,那麼很有可能在內存不足的時候直接就給淘汰掉了,所以默認情況下新分配的key的後8位計數器的值爲5(應該可配置),防止因爲訪問頻率過低而直接被刪除。 低8位我們描述完了,那麼高16位的時鐘是用來幹嘛的呢?目前我的理解是用來衰減低8位的計數器的,就是根據這個時鐘與全局時鐘進行比較,如果過了一定時間(做差)就會對計數器進行衰減。 最後,redis會對內部時鐘最小的key進行淘汰(最小表示最不頻繁使用),注意這個過程也是根據策略隨機選擇鍵
Redis過期策略
定時刪除
定時刪除策略對內存是最友好的:通過使用定時器,定時刪除策略可以保證過期鍵會盡可能快地被刪除,並釋放過期鍵所佔用的內存 定時刪除策略的缺點是,它對CPU時間是最不友好的:在過期鍵比較多的情況下,刪除過期鍵這一行爲可能會佔用相當一部分CPU時間 創建一個定時器需要用到Redis服務器中的時間事件,而當前時間事件的實現方式——無序列表,查找一個事件的時間複雜度爲O(N)——並不能高效地處理大量時間事件
定期刪除
定期刪除策略是前兩種策略的一種整合和折中:
- 定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響
- 通過定期刪除過期鍵,定期刪除策略有效地減少了因爲過期鍵而帶來的內存浪費
定期刪除策略的難點是確定刪除操作執行的時長和頻率:
- 如果刪除操作執行得太頻繁,或者執行的時間太長,定期刪除策略就會退化成定時刪除策略,以至於將CPU時間過多地消耗在刪除過期鍵上面
- 如果刪除操作執行得太少,或者執行的時間太短,定期刪除策略又會和惰性刪除策略一樣,出現浪費內存的情況
Redis 默認會每秒進行十次過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是採用了一種簡單的貪心策略。
- 從過期字典中隨機 20 個 key;
- 刪除這 20 個 key 中已經過期的 key;
- 如果過期的 key 比率超過 1/4,那就重複步驟 1;
同時,爲了保證過期掃描不會出現循環過度,導致線程卡死現象,算法還增加了掃描時間的上限,默認不會超過 25ms。 如果某一時刻,有大量key同時過期,Redis 會持續掃描過期字典,造成客戶端響應卡頓,因此設置過期時間時,就儘量避免這個問題,在設置過期時間時,可以給過期時間設置一個隨機範圍,避免同一時刻過期。 1.1. 如何配置定期刪除執行時間間隔 redis的定時任務默認是10s執行一次,如果要修改這個值,可以在redis.conf中修改hz的值。 redis.conf中,hz默認設爲10,提高它的值將會佔用更多的cpu,當然相應的redis將會更快的處理同時到期的許多key,以及更精確的去處理超時。 hz的取值範圍是1~500,通常不建議超過100,只有在請求延時非常低的情況下可以將值提升到100。 1.2 單線程的redis,如何知道要運行定時任務? redis是單線程的,線程不但要處理定時任務,還要處理客戶端請求,線程不能阻塞在定時任務或處理客戶端請求上,那麼,redis是如何知道何時該運行定時任務的呢? Redis 的定時任務會記錄在一個稱爲最小堆的數據結構中。這個堆中,最快要執行的任務排在堆的最上方。在每個循環週期,Redis 都會將最小堆裏面已經到點的任務立即進行處理。處理完畢後,將最快要執行的任務還需要的時間記錄下來,這個時間就是接下來處理客戶端請求的最大時長,若達到了該時長,則暫時不處理客戶端請求而去運行定時任務。
惰性刪除
惰性刪除策略對CPU時間來說是最友好的:程序只會在取出鍵時纔對鍵進行過期檢查,這可以保證刪除過期鍵的操作只會在非做不可的情況下進行,並且刪除的目標僅限於當前處理的鍵,這個策略不會再刪除其他無關的過期鍵上花費任何CPU時間 惰性刪除策略的缺點是,它對內存是最不友好的:如果一個鍵已經過期,而這個鍵又仍然保留在數據庫中,那麼只要這個過期鍵不被刪除,它所佔用的內存就不會釋放
Redis持久化
redis的持久化有兩種方式,分別是rdb和aof,區別如下
- RDB(redis database)
將緩存放到一個文件中,默認一段時間去存儲一次 會將內容先放到緩存文件,持久化結束之後,就用緩存文件代替上一次的持久化文件 優點:會調用子進程來保持持久化,不會有數據庫I/O 缺點:如果持久化的時候數據庫丟失了數據,因爲是’覆蓋的‘所以,就找不到數據了,故適用於不太重要的數據 簡單來說: rdb文件小,易備份,易恢復,恢復快
- AOF(append only file)
默認每秒去存儲歷史命令 保存的是數據的歷史指令,恢復數據的時候是將命令從前到後在執行一遍 優點:遇突發情況的話能找到以前的記錄,且數據丟失較少(1s) 缺點:每次都有IO操作,對服務器壓力較大 總結:回覆慢,數據完整性好,不易備份
RDB-Redis DataBase(內存快照)
所謂內存快照,就是指內存中的數據在某一個時刻的狀態記錄
RDB 文件的生成是否會阻塞主線程
Redis 提供了兩個命令來生成 RDB 文件,分別是 save 和 bgsave。
- save:在主線程中執行,會導致阻塞;
- bgsave:創建一個子進程,專門用於寫入 RDB 文件,避免了主線程的阻塞,這也是 Redis RDB 文件生成的默認配置。
這個時候,我們就可以通過 bgsave 命令來執行全量快照,這既提供了數據的可靠性保證,也避免了對 Redis 的性能影響。
快照時數據能修改嗎?
如果快照時數據不能被修改的話,會造成什麼後果,那肯定會對業務服務造成巨大的影響。Redis肯定是能支持快照時數據被修改的。這個時候,Redis 就會藉助操作系統提供的寫時複製技術(Copy-On-Write, COW),在執行快照的同時,正常處理寫操作。
簡單來說,bgsave 子進程是由主線程 fork 生成的,可以共享主線程的所有內存數據。bgsave 子進程運行後,開始讀取主線程的內存數據,並把它們寫入 RDB 文件。 此時,如果主線程對這些數據也都是讀操作(例如圖中的鍵值對 A),那麼,主線程和 bgsave 子進程相互不影響。但是,如果主線程要修改一塊數據(例如圖中的鍵值對 C),那麼,這塊數據就會被複制一份,生成該數據的副本。然後,bgsave 子進程會把這個副本數據寫入 RDB 文件,而在這個過程中,主線程仍然可以直接修改原來的數據。
多久一次
對於快照來說,快照的間隔時間變得很短,即使某一時刻發生宕機了,因爲上一時刻快照剛執行,丟失的數據也不會太多。但是,這其中的快照間隔時間就很關鍵了。
頻繁的快照其實是不好的,可以從下面兩個方面來說明。
- 頻繁將全量數據寫入磁盤,會給磁盤帶來壓力,多個快照競爭有限的磁盤帶寬,前一個快照還沒有做完,後一個又開始做了,容易造成惡性循環。
- bgsave 子進程需要通過 fork 操作從主線程創建出來。雖然,子進程在創建後不會再s阻塞主線程,但是,fork 這個創建過程本身會阻塞主線程,而且主線程的內存越大,阻塞時間越長。如果頻繁 fork 出 bgsave 子進程,這就會頻繁阻塞主線程了。
增量快照,就是指,做了一次全量快照後,後續的快照只對修改的數據進行快照記錄,這樣可以避免每次全量快照的開銷。
在第一次做完全量快照後,T1 和 T2 時刻如果再做快照,我們只需要將被修改的數據寫入快照文件就行。但是,這麼做的前提是,我們需要記住哪些數據被修改了。它需要我們使用額外的元數據信息去記錄哪些數據被修改了,這會帶來額外的空間開銷問題。如下圖所示
如果我們對每一個鍵值對的修改,都做個記錄,那麼,如果有 1 萬個被修改的鍵值對,我們就需要有 1 萬條額外的記錄。而且,有的時候,鍵值對非常小,比如只有 32 字節,而記錄它被修改的元數據信息,可能就需要 8 字節,這樣的話,爲了“記住”修改,引入的額外空間開銷比較大。這對於內存資源寶貴的 Redis 來說,有些得不償失。
Redis 4.0 中提出了一個混合使用 AOF 日誌和內存快照的方法。簡單來說,內存快照以一定的頻率執行,在兩次快照之間,使用 AOF 日誌記錄這期間的所有命令操作。這樣一來,快照不用很頻繁地執行,這就避免了頻繁 fork 對主線程的影響。 而且,AOF 日誌也只用記錄兩次快照間的操作,也就是說,不需要記錄所有操作了,因此,就不會出現文件過大的情況了,也可以避免重寫開銷。如下圖所示,T1 和 T2 時刻的修改,用 AOF 日誌記錄,等到第二次做全量快照時,就可以清空 AOF 日誌,因爲此時的修改都已經記錄到快照中了,恢復時就不再用日誌了
AOF()
Redis 將所有對數據庫進行過寫入的命令(及其參數)記錄到 AOF 文件, 以此達到記錄數據庫狀態的目的。
AOF持久化功能的實現可以分爲命令追加、文件寫入、文件同步三個步驟。
命令追加:
當AOF持久化功能打開時,服務器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩衝區的末尾。
AOF文件的寫入與同步:
每當服務器常規任務函數被執行、 或者事件處理器被執行時, aof.c/flushAppendOnlyFile 函數都會被調用, 這個函數執行以下兩個工作: WRITE:根據條件,將 aof_buf 中的緩存寫入到 AOF 文件。 SAVE:根據條件,調用 fsync 或 fdatasync 函數,將 AOF 文件保存到磁盤中。 兩個步驟都需要根據一定的條件來執行, 而這些條件由 AOF 所使用的保存模式來決定, 以下小節就來介紹 AOF 所使用的三種保存模式, 以及在這些模式下, 步驟 WRITE 和 SAVE 的調用條件。 Redis 目前支持三種 AOF 保存模式,它們分別是: AOF_FSYNC_NO :不保存。 AOF_FSYNC_EVERYSEC :每一秒鐘保存一次。 AOF_FSYNC_ALWAYS :每執行一個命令保存一次。
不保存
在這種模式下, 每次調用 flushAppendOnlyFile 函數, WRITE 都會被執行, 但 SAVE 會被略過。 在這種模式下, SAVE 只會在以下任意一種情況中被執行:
- Redis 被關閉
- AOF 功能被關閉
- 系統的寫緩存被刷新(可能是緩存已經被寫滿,或者定期保存操作被執行)
這三種情況下的 SAVE 操作都會引起 Redis 主進程阻塞。
每一秒鐘保存一次
在這種模式中, SAVE 原則上每隔一秒鐘就會執行一次, 因爲 SAVE 操作是由後臺子線程調用的, 所以它不會引起服務器主進程阻塞。 注意, 在上一句的說明裏面使用了詞語“原則上”, 在實際運行中, 程序在這種模式下對 fsync 或 fdatasync 的調用並不是每秒一次, 它和調用 flushAppendOnlyFile 函數時 Redis 所處的狀態有關。 每當 flushAppendOnlyFile 函數被調用時, 可能會出現以下四種情況: 子線程正在執行 SAVE ,並且: 這個 SAVE 的執行時間未超過 2 秒,那麼程序直接返回,並不執行 WRITE 或新的 SAVE 。 這個 SAVE 已經執行超過 2 秒,那麼程序執行 WRITE ,但不執行新的 SAVE 。注意,因爲這時 WRITE 的寫入必須等待子線程先完成(舊的) SAVE ,因此這裏 WRITE 會比平時阻塞更長時間。 子線程沒有在執行 SAVE ,並且: 上次成功執行 SAVE 距今不超過 1 秒,那麼程序執行 WRITE ,但不執行 SAVE 。 上次成功執行 SAVE 距今已經超過 1 秒,那麼程序執行 WRITE 和 SAVE 。 根據以上說明可以知道, 在“每一秒鐘保存一次”模式下, 如果在情況 1 中發生故障停機, 那麼用戶最多損失小於 2 秒內所產生的所有數據。 如果在情況 2 中發生故障停機, 那麼用戶損失的數據是可以超過 2 秒的。 Redis 官網上所說的, AOF 在“每一秒鐘保存一次”時發生故障, 只丟失 1 秒鐘數據的說法, 實際上並不準確。
每執行一個命令保存一次
在這種模式下,每次執行完一個命令之後, WRITE 和 SAVE 都會被執行。 另外,因爲 SAVE 是由 Redis 主進程執行的,所以在 SAVE 執行期間,主進程會被阻塞,不能接受命令請求。
AOF 文件的讀取和數據還原
AOF 文件保存了 Redis 的數據庫狀態, 而文件裏面包含的都是符合 Redis 通訊協議格式的命令文本。 這也就是說, 只要根據 AOF 文件裏的協議, 重新執行一遍裏面指示的所有命令, 就可以還原 Redis 的數據庫狀態了。 Redis 讀取 AOF 文件並還原數據庫的詳細步驟如下:
- 創建一個不帶網絡連接的僞客戶端(fake client)。
- 讀取 AOF 所保存的文本,並根據內容還原出命令、命令的參數以及命令的個數。
- 根據命令、命令的參數和命令的個數,使用僞客戶端執行該命令。
- 執行 2 和 3 ,直到 AOF 文件中的所有命令執行完畢。
完成第 4 步之後, AOF 文件所保存的數據庫就會被完整地還原出來。 注意, 因爲 Redis 的命令只能在客戶端的上下文中被執行, 而 AOF 還原時所使用的命令來自於 AOF 文件, 而不是網絡, 所以程序使用了一個沒有網絡連接的僞客戶端來執行命令。 僞客戶端執行命令的效果, 和帶網絡連接的客戶端執行命令的效果, 完全一樣。
AOF重寫
因爲AOF持久化是通過保存被執行的寫命令來記錄數據庫狀態的,所以隨着服務器運行時間的流逝,AOF文件中的內容越來越多,文件體積越來越大,如果不加以控制,會對redis服務器甚至宿主計算器造成影響。 所謂的“重寫”其實是一個有歧義的詞語, 實際上, AOF 重寫並不需要對原有的 AOF 文件進行任何寫入和讀取, 它針對的是數據庫中鍵的當前值。 AOF重寫程序aof_rewrite函數可以很好完成創建一個新AOF文件的任務,但是這個函數會進行大量寫入操作,會長時間阻塞,所以Redis將AOF重寫程序放到子進程裏執行,這樣做達到兩個目的:
- 子進程AOF重寫期間,服務器進程可以繼續處理命令請求。
- 子進程帶有數據庫進程的數據副本,使用子進程而不是線程,可以避免使用鎖的情況下保證數據安全。
不過,使用子進程也有一個問題需要解決,就是AOF重寫期間如果有新的寫命令進來,不能漏掉,那樣會數據不一致。 於是Redis服務器設置了一個AOF重寫緩衝區 最後流程變爲:
- 執行客戶端發來的命令
- 將執行的寫命令追加到AOF緩衝區
- 將執行後的寫命令追加到AOF重寫緩衝區
這樣一來可以保證:
- AOF緩衝區的內容會定期被寫入和同步到AOF文件,對現有AOF文件的處理工作會照常進行。
- 從創建子進程開始,服務器執行的所有寫命令會被記錄到AOF重寫緩衝區裏面
當子進程完成AOF重寫工作之後,它會向父進程發送一個信號,父進程收到信號後,會調用一個信號處理函數,並執行以下工作:
- 將AOF重寫緩衝區中的所有內容寫入新的AOF文件中,這時新AOF文件鎖保存的數據庫狀態和服務器當前狀態一致
- 對新的AOF文件進行改名,原子性操作地覆蓋現有的AOF文件,完成新舊AOF文件的替換。
這個信號函數執行完畢以後,父進程就可以繼續像往常一樣接受命令請求了,在整個AOF後臺重寫過程中,只有信號處理函數執行時會對服務器進程造成阻塞,其他時候都可以繼續處理請求,這樣AOF重寫對服務器性能造成的影響降到了最低。 以上就是AOF後臺重寫,也即是BGREWRITEAOF命令的實現原理。
AOF的缺點
·對於相同的數據集來說,AOF文件的體積通常要大於 RDB文件的體積。 ·根據所使用的Fsync策略,AOF的速度可能會慢於 RDB。在一般情況下,每秒Fsync的性能依然非常高,而關閉 Fsync可以讓 AOF的速度和 RDB一樣快,即使在高負荷之下也是如此。不過在處理巨大的寫入載入時,RDB可以提供更有保證的最大延遲時間(Latency)。
https://blog.csdn.net/luolaifa000/article/details/84178289 https://blog.csdn.net/qq_35433716/article/details/82191511 https://blog.csdn.net/qq_42327755/article/details/108994089