天天看點

論程式的健壯性——就看Redis

“衆裡尋他千百度,蓦然回首,那人卻在,燈火闌珊處”。多年的IT生涯,一直希望自己寫的程式能夠有很強的健壯性,也一直希望能找到一個高可用的标杆程式去借鑒學習,不畏懼記憶體溢出、磁盤滿了、斷網、斷電、機器重新開機等等情況。但意想不到的是,這個标杆程式竟然就是從一開始就在使用的分布式緩存——Redis。
論程式的健壯性——就看Redis

Redis(Remote Dictionary Server ),即遠端字典服務,是 C 語言開發的一個開源的高性能鍵值對(key-value)的記憶體資料庫。由于它是基于記憶體的是以它要比基于磁盤讀寫的資料庫效率更快。是以Redis也就成了大家解決資料庫高并發通路、分布式讀寫和分布式鎖等首選解決方案。

那麼既然它是基于記憶體的,如果記憶體滿了怎麼辦?程式會不會崩潰?既然它是基于記憶體的,如果伺服器當機了怎麼辦?資料是不是就丢失了?既然它是分布式的,這台Redis伺服器斷網了怎麼辦?

今天我們就一起來看看Redis的設計者,一名來自意大利的小夥,是如何打造出一個超強健壯性和高可用性的程式,進而不懼怕這些情況。

一、 Redis的記憶體管理政策——記憶體永不溢出

Redis主要有兩種政策機制來保障存儲的key-value資料不會把記憶體塞滿,它們是:過期政策和淘汰政策。

1、 過期政策

用過Redis的人都知道,我們往Redis裡添加key-value的資料時,會有個選填參數——過期時間。如果設定了這個參數的值,Redis到過期時間後會自行把過期的資料給清除掉。“過期政策”指的就是Redis内部是如何實作将過期的key對應的緩存資料清除的。

在Redis源碼中有三個核心的對象結構:redisObject、redisDb和serverCron。

  • redisObject:Redis 内部使用redisObject 對象來抽象表示所有的 key-value。簡單地說,redisObject就是string、hash、list、set、zset的父類。為了便于操作,Redis采用redisObject結構來統一這五種不同的資料類型。
論程式的健壯性——就看Redis
  • redisDb:Redis是一個鍵值對資料庫伺服器,這個資料庫就是用redisDb抽象表示的。redisDb結構中有很多dict字典儲存了資料庫中的所有鍵值對,這些字典就叫做鍵空間。如下圖所示其中有個“expires”的字典就儲存了設定過期時間的鍵值對。而Redis的過期政策也是圍繞它來進行的。
論程式的健壯性——就看Redis
  • serverCron:Redis 将serverCron作為時間事件來運作,進而確定它每隔一段時間就會自動運作一次。是以redis中所有定時執行的事件任務都在serverCron中執行。

了解完Redis的三大核心結構後,咱們回到“過期政策”的具體實作上,其實Redis主要是靠兩種機制來處理過期的資料被清除:定期過期(主動清除)和惰性過期(被動清除)。

  • 惰性過期(被動清除):就是每次通路的時候都去判斷一下該key是否過期,如果過期了就删除掉。該政策就可以最大化地節省CPU資源,但是卻對記憶體非常不友好。因為不實時過期了,原本該過期删除的就可能一直堆積在記憶體裡面!極端情況可能出現大量的過期key沒有再次被通路,進而不會被清除,占用大量記憶體。
  • 定期過期(主動清除):每隔一定的時間,會掃描Redis資料庫的expires字典中一定數量的key,并清除其中已過期的 key。Redis預設配置會每100毫秒進行1次(redis.conf 中通過 hz 配置)過期掃描,掃描并不是周遊過期字典中的所有鍵,而是采用了如下方法:

(1)從過期字典中随機取出20個鍵;

(server.h檔案下

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP

配置20)

(2)删除這20個鍵中過期的鍵;

(3)如果過期鍵的比例超過 25% ,重複步驟 1 和 2;

具體邏輯如下圖:

論程式的健壯性——就看Redis

因為Redis中同時使用了惰性過期和定期過期兩種過期政策,是以在不同情況下使得 CPU 和記憶體資源達到最優的平衡效果的同時,保證過期的資料會被及時清除掉。

2、淘汰政策

在Redis可能沒有需要過期的資料的情況下,還是會把我們的記憶體都占滿。比如每個key設定的過期時間都很長或不過期,一直添加就有可能把記憶體給塞滿。那麼Redis又是怎麼解決這個問題的呢?——那就是“淘汰政策”。

論程式的健壯性——就看Redis

官網位址:

https://redis.io/topics/lru-cache

Reids官網上面列出的淘汰政策一共有8種,但從實質算法來看隻有兩種實作算法,分别是LRU和LFU。

LRU(Least Recently Used):翻譯過來是最久未使用,根據時間軸來走,淘汰那些距離上一次使用時間最久遠的資料。

LRU的簡單原理如下圖:

論程式的健壯性——就看Redis

從上圖我們可以看出,在容器滿了的情況下,距離上次讀寫時間最久遠的E被淘汰掉了。那麼資料每次讀取或者插入都需要擷取一下目前系統時間,以及每次淘汰的時候都需要拿目前系統時間和各個資料的最後操作時間做對比,這麼幹勢必會增加CPU的負荷進而影響Redis的性能。Redis的設計者為了解決這一問題,做了一定的改善,整體的LRU思路如下:

(1)、Redis裡設定了一個全局變量 server.lruclock 用來存放系統目前的時間戳。這個全局變量通過serverCron 每100毫秒調用一次updateCachedTime()更新一次值。

(2)、每當redisObject資料被讀或寫的時候,将目前的 server.lruclock值指派給 redisObject 的lru屬性,記錄這個資料最後的lru值。

(3)、觸發淘汰政策時,随機從資料庫中選擇采樣值配置個數key, 淘汰其中熱度最低的key對應的緩存資料。

注:熱度就是拿目前的全局server.lruclock 值與各個資料的lru屬性做對比,相差最久遠的就是熱度最低的。
論程式的健壯性——就看Redis

Redis中所有對象結構都有一個lru字段, 且使用了unsigned的低24位,這個字段就是用來記錄對象的熱度。

LFU(Least Frequently Used):翻譯成中文就是最不常用。是按着使用頻次來算的,淘汰那些使用頻次最低的資料。說白了就是“末尾淘汰制”!

剛才講過的LRU按照最久未使用雖然能達到淘汰資料釋放空間的目的,但是它有一個比較大的弊端,如下圖:

論程式的健壯性——就看Redis

如圖所示A在10秒内被通路了5次,而B在10秒内被通路了3 次。因為 B 最後一次被通路的時間比A要晚,在同等的情況下,A反而先被回收。那麼它就是不合理的。LFU就完美解決了LRU的這個弊端,具體原理如下:

論程式的健壯性——就看Redis

上圖是末尾淘汰的原理示意圖,僅是按次數這個次元做的末尾淘汰,但如果Redis僅按使用次數,也會有一個問題,就是某個資料之前被通路過很多次比如上萬次,但後續就一直不用了,它本身按使用頻次來講是應該被淘汰的。是以Redis在實作LFU時,用兩部分資料來标記這個資料:使用頻率和上次通路時間。整體思路就是:有讀寫我就增加熱度,一段時間内沒有讀寫我就減少相應熱度。

論程式的健壯性——就看Redis

不管是LRU還是LFU淘汰政策,Redis都是用lru這個字段實作的具體邏輯,如果配置的淘汰政策是LFU時,lru的低8位代表的是頻率,高16位就是記錄上次通路時間。整體的LRU思路如下:

(1)每當資料被寫或讀的時候都會調用LFULogIncr(counter)方法,增加lru低8位的通路頻率數值;具體每次增加的數值在redis.conf中配置預設是10(# lfu-log-factor 10)

(2)還有另外一個配置lfu-decay-time 預設是1分鐘,來控制每隔多久沒人通路則熱度會遞減相應數值。這樣就規避了一個超大通路次數的資料很久都不被淘汰的漏洞。

小結:“過期政策” 保證過期的key對應的資料會被及時清除;“淘汰政策”保證記憶體滿的時候會自動釋放相應空間,是以Redis的記憶體可以自運作保證不會産生溢出異常。

二、 Redis的資料持久化政策——當機可立即恢複資料到記憶體

有了記憶體不會溢出保障後,我們再來看看Redis是如何保障伺服器當機或重新開機,原來緩存在記憶體中的資料是不會丢失的。也就是Redis的持久化機制。

Redis 的持久化政策有兩種:RDB(快照全量持久化)和AOF(增量日志持久化)

1、 RDB

RDB 是 Redis 預設的持久化方案。RDB快照(Redis DataBase),當觸發一定條件的時候,會把目前記憶體中的資料寫入磁盤,生成一個快照檔案dump.rdb。Redis重新開機會通過dump.rdb檔案恢複資料。那那個一定的條件是啥呢?到底什麼時候寫入rdb 檔案?

觸發Redis執行rdb的方式有兩類:自動觸發和手動觸發

“自動觸發”的情況有三種:達到配置檔案觸發規則時觸發、執行shutdown指令時觸發、執行flushall指令時觸發。

注:在redis.conf中有個 SNAPSHOTTING配置,其中定義了觸發把資料儲存到磁盤觸發頻率。

“手動觸發”的方式有兩種:執行save 或 bgsave指令。執行save指令在生成快照的時候會阻塞目前Redis伺服器,Redis不能處理其他指令。如果記憶體中的資料比較多,會造成Redis長時間的阻塞。生産環境不建議使用這個指令。

為了解決這個問題,Redis 提供了第二種方式bgsave指令進行資料備份,執行bgsave時,Redis會在背景異步進行快照操作,快照同時還可以響應用戶端請求。

具體操作是Redis程序執行fork(建立程序函數)操作建立子程序(copy-on-write),RDB持久化過程由子程序負責,完成後自動結束。它不會記錄 fork 之後後續的指令。阻塞隻發生在fork階段,一般時間很短。手動觸發的場景一般僅用在遷移資料時才會用到。

我們知道了RDB的實作的原理邏輯,那麼我們就來分析下RDB到底有什麼優劣勢。

優勢:

(1)RDB是一個非常緊湊(compact類型)的檔案,它儲存了redis在某個時間點上的資料集。這種檔案非常适合用于進行備份和災難恢複。

(2)生成RDB檔案的時候,redis主程序會fork()一個子程序來處理所有儲存工作,主程序不需要進行任何磁盤IO操作。

(3)RDB在恢複大資料集時的速度比AOF的恢複速度要快。

劣勢:

RDB方式資料沒辦法做到實時持久化/秒級持久化。在一定間隔時間做一次備份,是以如果Redis意外down掉的話,就會丢失最後一次快照之後的所有修改

2、 AOF(Append Only File)

AOF采用日志的形式來記錄每個寫操作的指令,并追加到檔案中。開啟後,執行更改 Redis資料的指令時,就會把指令寫入到AOF檔案中。Redis重新開機時會根據日志檔案的内容把寫指令從前到後執行一次以完成資料的恢複工作。

論程式的健壯性——就看Redis

其實AOF也不一定是完全實時的備份操作指令,在redis.conf 我們可以配置選擇 AOF的執行方式,主要有三種:always、everysec和no

AOF是追加更改指令檔案,那麼大家想下一直追加追加,就是會導緻檔案過大,那麼Redis是怎麼解決這個問題的呢?

Redis解決這個問題的方法是AOF下面有個機制叫做bgrewriteaof重寫機制,我們來看下它是個啥

論程式的健壯性——就看Redis
注:AOF檔案重寫并不是對原檔案進行重新整理,而是直接讀取伺服器現有的鍵值對,然後用一條指令去代替之前記錄這個鍵值對的多條指令,生成一個新的檔案後去替換原來的AOF檔案。

我們知道了AOF的實作原理,我們來分析下它的優缺點。

優點:

能最大限度的保證資料安全,就算用預設的配置everysec,也最多隻會造成1s的資料丢失。

缺點:

資料量比RDB要大很多,是以性能沒有RDB好!

論程式的健壯性——就看Redis
小結:因為有了持久化機制,是以Redis即使伺服器當機或重新開機了,也可以最大限度的恢複資料到記憶體中,提供給client繼續使用。

三、Redis的哨兵模式——可戰到最後一兵一卒的高可用叢集

記憶體滿了不會挂,伺服器當機重新開機也沒問題。足見Redis的程式健壯性已經足夠強大。但Redis的設計者,在面向高可用面前,仍繼續向前邁進了一步,那就是Redis的高可用叢集方案——哨兵模式。

所謂的“哨兵模式”就是有一群哨兵(Sentinel)在Redis伺服器前面幫我們監控這Redis叢集各個機器的運作情況,并且哨兵間互相通告通知,并指引我們使用那些健康的服務。

論程式的健壯性——就看Redis

Sentinel工作原理:

1、 Sentinel 預設以每秒鐘1次的頻率向Redis所有服務節點發送 PING 指令。如果在down-after-milliseconds 内都沒有收到有效回複,Sentinel會将該伺服器标記為下線(主觀下線)。

2、 這個時候Sentinel節點會繼續詢問其他的Sentinel節點,确認這個節點是否下線, 如果多數 Sentinel節點都認為master下線,master才真正确認被下線(客觀下線),這個時候就需要重新選舉master。

Sentinel的作用:

1、監控:Sentinel 會不斷檢查主伺服器和從伺服器是否正常運作

2、故障處理:如果主伺服器發生故障,Sentinel可以啟動故障轉移過程。把某台伺服器更新為主伺服器,并發出通知

3、配置管理:用戶端連接配接到 Sentinel,擷取目前的 Redis 主伺服器的位址。我們不是直接去擷取Redis主服務的位址,而是根據sentinel去自動擷取誰是主機,即使主機發生故障後我們也不用改代碼的連接配接!

論程式的健壯性——就看Redis
小結:有了“哨兵模式”隻要叢集中有一個Redis伺服器還健康存活,哨兵就能把這個健康的Redis伺服器提供給我們(如上圖的1、2兩步),那麼我們用戶端的連結就不會出錯。是以,Redis叢集可以戰鬥至最後一兵一卒。

這就是Redis,一個“高可用、強健壯性”的标杆程式!

作者:宜信技術學院 譚文濤