redis事件機制(淘汰、過期、持久化)

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)——並不能高效地處理大量時間事件 ​

定期刪除

定期刪除策略是前兩種策略的一種整合和折中:

  1. 定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響
  2. 通過定期刪除過期鍵,定期刪除策略有效地減少了因爲過期鍵而帶來的內存浪費

定期刪除策略的難點是確定刪除操作執行的時長和頻率:

  1. 如果刪除操作執行得太頻繁,或者執行的時間太長,定期刪除策略就會退化成定時刪除策略,以至於將CPU時間過多地消耗在刪除過期鍵上面
  2. 如果刪除操作執行得太少,或者執行的時間太短,定期刪除策略又會和惰性刪除策略一樣,出現浪費內存的情況

Redis 默認會每秒進行十次過期掃描,過期掃描不會遍歷過期字典中所有的 key,而是採用了一種簡單的貪心策略。

  1. 從過期字典中隨機 20 個 key;
  2. 刪除這 20 個 key 中已經過期的 key;
  3. 如果過期的 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 文件,而在這個過程中,主線程仍然可以直接修改原來的數據。

多久一次

對於快照來說,快照的間隔時間變得很短,即使某一時刻發生宕機了,因爲上一時刻快照剛執行,丟失的數據也不會太多。但是,這其中的快照間隔時間就很關鍵了。 ​

頻繁的快照其實是不好的,可以從下面兩個方面來說明。

  1. 頻繁將全量數據寫入磁盤,會給磁盤帶來壓力,多個快照競爭有限的磁盤帶寬,前一個快照還沒有做完,後一個又開始做了,容易造成惡性循環。
  2. bgsave 子進程需要通過 fork 操作從主線程創建出來。雖然,子進程在創建後不會再s阻塞主線程,但是,fork 這個創建過程本身會阻塞主線程,而且主線程的內存越大,阻塞時間越長。如果頻繁 fork 出 bgsave 子進程,這就會頻繁阻塞主線程了。

增量快照,就是指,做了一次全量快照後,後續的快照只對修改的數據進行快照記錄,這樣可以避免每次全量快照的開銷。 ​

在第一次做完全量快照後,T1 和 T2 時刻如果再做快照,我們只需要將被修改的數據寫入快照文件就行。但是,這麼做的前提是,我們需要記住哪些數據被修改了。它需要我們使用額外的元數據信息去記錄哪些數據被修改了,這會帶來額外的空間開銷問題。如下圖所示 image.png

如果我們對每一個鍵值對的修改,都做個記錄,那麼,如果有 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 只會在以下任意一種情況中被執行:

  1. Redis 被關閉
  2. AOF 功能被關閉
  3. 系統的寫緩存被刷新(可能是緩存已經被寫滿,或者定期保存操作被執行)

這三種情況下的 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 。 image.png 根據以上說明可以知道, 在“每一秒鐘保存一次”模式下, 如果在情況 1 中發生故障停機, 那麼用戶最多損失小於 2 秒內所產生的所有數據。 如果在情況 2 中發生故障停機, 那麼用戶損失的數據是可以超過 2 秒的。 Redis 官網上所說的, AOF 在“每一秒鐘保存一次”時發生故障, 只丟失 1 秒鐘數據的說法, 實際上並不準確。 ​

每執行一個命令保存一次

在這種模式下,每次執行完一個命令之後, WRITE 和 SAVE 都會被執行。 另外,因爲 SAVE 是由 Redis 主進程執行的,所以在 SAVE 執行期間,主進程會被阻塞,不能接受命令請求。 image.png

AOF 文件的讀取和數據還原

AOF 文件保存了 Redis 的數據庫狀態, 而文件裏面包含的都是符合 Redis 通訊協議格式的命令文本。 這也就是說, 只要根據 AOF 文件裏的協議, 重新執行一遍裏面指示的所有命令, 就可以還原 Redis 的數據庫狀態了。 Redis 讀取 AOF 文件並還原數據庫的詳細步驟如下:

  1. 創建一個不帶網絡連接的僞客戶端(fake client)。
  2. 讀取 AOF 所保存的文本,並根據內容還原出命令、命令的參數以及命令的個數。
  3. 根據命令、命令的參數和命令的個數,使用僞客戶端執行該命令。
  4. 執行 2 和 3 ,直到 AOF 文件中的所有命令執行完畢。

完成第 4 步之後, AOF 文件所保存的數據庫就會被完整地還原出來。 注意, 因爲 Redis 的命令只能在客戶端的上下文中被執行, 而 AOF 還原時所使用的命令來自於 AOF 文件, 而不是網絡, 所以程序使用了一個沒有網絡連接的僞客戶端來執行命令。 僞客戶端執行命令的效果, 和帶網絡連接的客戶端執行命令的效果, 完全一樣。

AOF重寫

因爲AOF持久化是通過保存被執行的寫命令來記錄數據庫狀態的,所以隨着服務器運行時間的流逝,AOF文件中的內容越來越多,文件體積越來越大,如果不加以控制,會對redis服務器甚至宿主計算器造成影響。 所謂的“重寫”其實是一個有歧義的詞語, 實際上, AOF 重寫並不需要對原有的 AOF 文件進行任何寫入和讀取, 它針對的是數據庫中鍵的當前值。 AOF重寫程序aof_rewrite函數可以很好完成創建一個新AOF文件的任務,但是這個函數會進行大量寫入操作,會長時間阻塞,所以Redis將AOF重寫程序放到子進程裏執行,這樣做達到兩個目的:

  1. 子進程AOF重寫期間,服務器進程可以繼續處理命令請求。
  2. 子進程帶有數據庫進程的數據副本,使用子進程而不是線程,可以避免使用鎖的情況下保證數據安全。

不過,使用子進程也有一個問題需要解決,就是AOF重寫期間如果有新的寫命令進來,不能漏掉,那樣會數據不一致。 於是Redis服務器設置了一個AOF重寫緩衝區 最後流程變爲:

  1. 執行客戶端發來的命令
  2. 將執行的寫命令追加到AOF緩衝區
  3. 將執行後的寫命令追加到AOF重寫緩衝區

這樣一來可以保證:

  1. AOF緩衝區的內容會定期被寫入和同步到AOF文件,對現有AOF文件的處理工作會照常進行。
  2. 從創建子進程開始,服務器執行的所有寫命令會被記錄到AOF重寫緩衝區裏面

當子進程完成AOF重寫工作之後,它會向父進程發送一個信號,父進程收到信號後,會調用一個信號處理函數,並執行以下工作:

  1. 將AOF重寫緩衝區中的所有內容寫入新的AOF文件中,這時新AOF文件鎖保存的數據庫狀態和服務器當前狀態一致
  2. 對新的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

https://redisbook.readthedocs.io/en/latest/index.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章