天天看點

與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

作者:晾幹的紅領巾

一、Redis基本概念

  • 面試官心理: 靠!手上活都沒幹完又叫我過來面試,這不耽誤我事麼,今兒又得加班補活了........咦,這小夥子履歷不錯啊,先考考它Redis..........
  • 面試官: 談談你對Redis的了解?
  • 我: Redis是ANSI C語言編寫的一個基于記憶體的高性能鍵值對(key-value)的NoSQL資料庫,一般用于架設在Java程式與資料庫之間用作緩存層,為了防止DB磁盤IO效率過低造成的請求阻塞、響應緩慢等問題,用來彌補DB與Java程式之間的性能差距,同時,也可以在DB吞吐跟不上系統并發量時,避免請求直接落入DB進而起到保護DB的作用。
  • 而Redis一般除了緩存DB資料之外還可以利用它豐富的資料類型及指令來實作一些其他功能,比如:計數器、使用者線上狀态、排行榜、session存儲等,同時Redis的性能也非常可觀,通過官方給出的資料顯示能夠達到10w/s的QPS處理,但是在生産環境的實測結果大概讀取QPS在7-9w/s,寫入QPS在6-8w/s左右(注:與機器性能也有關),同時Redis也提供事務、持久化、高可用等一些機制的支援。

二、Redis基本資料類型與常用指令

  • 面試官: 剛剛聽你提到了可以利用它豐富的資料類型及指令來實作一些其他功能,那你跟我講講Redis的一些常用指令。
  • 我: Redis常用的一些指令的話一般是都是對于基本資料類型的操作指令以及一些全局指令.....叭啦叭啦叭......,如下:
指令 作用
keys * 傳回所有鍵(keys還能用來搜尋,比如keys h*:搜尋所有以h開頭的鍵)
dbsize 傳回鍵數量,如果存在大量鍵,線上禁止使用此指令
exists key 檢查鍵是否存在,存在傳回 1,不存在傳回 0
del key 删除鍵,傳回删除鍵個數,删除不存在鍵傳回 0
ttl key 檢視鍵存活時間,傳回鍵剩餘過期時間,不存在傳回-1
expire key seconds 設定過期時間(機關:s),成功傳回1,失敗傳回0
expireat key timestamp 設定key在某個時間戳(精确到秒)之後過期
pexpire key milliseconds 設定過期時間(機關:ms),成功傳回1,失敗傳回0
persist key 去掉過期時間
monitor 實時監聽并傳回Redis伺服器接收到的所有請求資訊
shutdown 把資料同步儲存到磁盤上,并關閉Redis服務
info 檢視目前Redis節點資訊
....... .......
當然了,一般也是記得一些常用的指令,但是 更多指令參考:Redis指令大全,因為Redis指令和JVM參數一樣,隻要記得可以這樣做就行了,但是具體的可以去參考相關文檔資料。
  • 面試官: 嗯嗯,不錯,那再接着講講Redis的基本資料類型以及你是在項目中怎麼使用它們的吧!
  • 我: Redis資料類型在之前是五種,但是現在的版本中存在九種,分别為:字元串(strings/string)、散列(hashes/hash)、清單(lists/list)、集合(sets/set)、有序集合(sorted sets/zset)以及後續的四種資料類型:bitmaps、hyperloglogs、地理空間(geospatial)、消息(Streams),不過無論是哪種資料類型Redis都不會直接将它放在記憶體中存儲,而是轉而内部使用RedisObject來存儲以及表示所有類型的key-value(說着說着我拿出了紙和筆,給面試官畫了一張圖):
與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

Redis内部使用一個RedisObject對象來表示所有的key和value,RedisObject最主要的資訊如上圖所示:type表示一個value對象具體是何種資料類型,encoding是不同資料類型在Redis内部的存儲方式。比如:type=string表示value存儲的是一個普通字元串,那麼encoding可以是raw或者int,而關于其他資料類型的内部編碼實作我頓時再拿起筆chua~ chua~ chua:

與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!
  • 我接着回答: 下面我再簡單講講Redis的基本資料類型以及它們的應用場景:
類型 描述 特性 場景
string 二進制安全 可以存儲任何元素(數字、字元、音視訊、圖檔、對象.....) 計數器、分布式鎖、字元緩存、分布式ID生成、session共享、秒殺token、IP限流等
hash 鍵值對存儲,類似于Map集合 适合存儲對象,可以将對象屬性一個個存儲,更新時也可以更新單個屬性,操作某一個字段 對象緩存、購物車等
list 雙向連結清單 增删快 棧、隊列、有限集合、消息隊列、消息推送、阻塞隊列等
set 元素不能重複,每次擷取無序 添加、删除、查找的複雜度都是O(1),提供了求交集、并集、差集的操作 抽獎活動、朋友圈點贊、使用者(微網誌好友)關注、相關關注、共同關注、好友推薦(可能認識的人)等
sorted set 有序集合,每個元素有一個對應的分數,不允許元素重複 基于分數進行排序,如果分數相等,以key值的 ascii 值進行排序 商品評價标簽(好評、中評、差評等)、排行榜等
bitmaps Bitmaps是一個位元組由 8 個二進制位組成 在字元串類型上面定義的位操作 線上使用者統計、使用者通路統計、使用者點選統計等
hyperloglog Redis2.8.9版本添加了 HyperLogLog結構。Redis HyperLogLog是用來做基數統計的算法。 用于進行基數統計,不是集合,不儲存資料,隻記錄數量而不是具體資料 統計獨立UV等
geospatial Redis3.2版本新增的資料類型:GEO對地理位置的支援 以将使用者給定的地理位置資訊儲存起來, 并對這些資訊進行操作 地理位置計算
stream Redis5.0之後新增的資料類型 支援釋出訂閱,一對多消費 消息隊列
PS:HyperLogLog的優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定 的、并且是很小的。在 Redis 裡面,每個HyperLogLog鍵隻需要花費12 KB記憶體,就可以計算接近2^64個不同元素的基數。這和計算基數時,元素越多耗費記憶體就越多的集合形成鮮明對比。但是,因為HyperLogLog隻會根據輸入元素來計算基數,而不會儲存輸入元素本身,是以HyperLogLog不能像集合那樣,傳回輸入的各個元素(核心是基數估算算法,最終數值存在一定誤差誤差範圍:基數估計的結果是一個帶有0.81%标準錯誤的近似值,耗空間極小,每個hyperloglog key占用了12K的記憶體用于标記基數,pfadd指令不是一次性配置設定12K記憶體使用,會随着基數的增加記憶體逐漸增大,Pfmerge指令合并後占用的存儲空間為12K,無論合并之前資料量多少)

三、Redis緩存及一緻性、雪崩、擊穿與穿透問題

  • 面試官提問: 那麼你們在使用Redis做為緩存層的時候是怎麼通過Java操作Redis的呢?
  • 我的心理: 這問題不是送命題嗎.....
  • 我: Java操作Redis的用戶端有很多,比如springData中的RedisTemplate,也有SpringCache內建Redis後的注解形式,當然也會有一些Jedis、Lettuce、Redisson等等,而我們使用的是Lettuce以及Redisson........
  • 面試官提問: 那你們在使用Redis作為緩存的時候有沒有遇到什麼問題呢?
  • 我: 咳咳,是的,确實遇到了以及考慮到了一些問題,比如緩存一緻性、雪崩、穿透與擊穿,關于Redis與MySQL之間的資料一緻性問題其實也考慮過很多方案,比如先删後改,延時雙删等等很多方案,但是在高并發情況下還是會造成資料的不一緻性,是以關于DB與緩存之間的強一緻性一定要保證的話那麼就對于這部分資料不要做緩存,操作直接走DB,但是如果這個資料比較熱點的話那麼還是會給DB造成很大的壓力,是以在我們的項目中還是采用先删再改+過期的方案來做的,雖然也會存在資料的不一緻,但是勉強也能接受,因為畢竟使用緩存通路快的同時也能減輕DB壓力,而且本身采用緩存就需要接受一定的資料延遲性和短暫的不一緻性,我們隻能采取合适的政策來降低緩存和資料庫間資料不一緻的機率,而無法保證兩者間的強一緻性。合适的政策包括合适的緩存更新政策,合适的緩存淘汰政策,更新資料庫後及時更新緩存、緩存失敗時增加重試機制等。
  • 面試官話鋒一轉: 打斷一下,你剛剛提到了使用緩存能讓通路變快,那麼你能不能講講Redis為什麼快呢?
  • 我的心理: 好家夥,這一手來的我猝不及防......
  • 硬着頭發回答: Redis快的原因嘛其實可以從多個次元來看待:
    • 一、Redis完全基于記憶體
    • 二、Redis整個結構類似于HashMap,查找和操作複雜度為O(1),不需要和MySQL查找資料一樣需要産生随機磁盤IO或者全表
    • 三、Redis對于用戶端的處理是單線程的,采用單線程處理所有用戶端請求,避免了多線程的上下文切換和線程競争造成的開銷
    • 四、底層采用select/epoll多路複用的高效非阻塞IO模型
    • 五、用戶端通信協定采用RESP,簡單易讀,避免了複雜請求的解析開銷
  • 面試官露出姨父般的慈笑: 嗯嗯,還不錯,那你繼續談談剛剛的緩存雪崩、穿透與擊穿的問題吧
  • 我: 好的,先說緩存雪崩吧,緩存雪崩造成的原因是因為我們在做緩存時為了保證記憶體使用率,一般在寫入資料時都會給定一個過期時間,而就是因為過期時間的設定有可能導緻大量的熱點key在同一時間内全部失效,此時來了大量請求通路這些key,而Redis中卻沒有這些資料,進而導緻所有請求直接落入DB查詢,造成DB出現瓶頸或者直接被打宕導緻雪崩情況的發生。關于解決方案的的話也可以從多個次元來考慮:
    • 一、設定熱點資料永不過期,避免熱點資料的失效導緻大量的相同請求落入DB
    • 二、錯開過期時間的設定,根據業務以及線上情況合理的設定失效時間
    • 三、使用分布式鎖或者MQ隊列使得請求串行化,進而避免同一時間請求大量落入DB(性能會受到很大的影響)
  • 面試官: 那緩存穿透呢?指的是什麼?又該怎麼解決?
  • 我喝了口水接着回答: 緩存穿透這個問題是由于請求參數不合理導緻的,比如對外暴露了一個接口getUser?userID=xxx,而資料庫中的userID是從1開始的,當有黑客通過這個接口攜帶不存在的ID請求時,比如:getUser?userID=-1,請求會先來到Redis中查詢緩存,但是發現沒有對應的資料進而轉向DB查詢,但是DB中也無此值, 是以也無法寫入資料到緩存,而黑客就通過這一點利用“殭屍電腦”等手段瘋狂請求這個接口,導緻出現大量Redis不存在資料的請求落入DB,進而導緻DB出現瓶頸或者直接被打當機,整個系統陷入癱瘓。
  • 面試官: 嗯,那又該如果避免這種情況呢?
  • 我: 解決方案也有好幾種呢:
    • 一、做IP限流與黑名單,避免同一IP一瞬間發送大量請求
    • 二、對于請求做非法校驗,對于攜帶非法參數的請求直接過濾
    • 三、對于DB中查詢不存在的資料寫入Redis中“Not Data”并設定短暫的過期時間,下次請求能夠直接被攔截在Redis而不會落入DB
    • 四、布隆過濾器
  • 面試官: 那接下來的緩存擊穿呢?又是怎麼回事?怎麼解決?
  • 我: 這個簡單,緩存擊穿和緩存雪崩有點類似,都是由于請求的key過期導緻的問題,但是不同點在于失效key的數量,對于雪崩而言指的是大量的key失效導緻大量請求落入DB,而對于擊穿而言,指的是某一個熱點key突然過期,而這個時候又突然又大量的請求來查詢它,但是在Redis中卻并沒有查詢到結果進而導緻所有請求全部打向DB,導緻在這個時刻DB直接被打穿。解決方案的話也是有多種:
    • 一、設定熱點key永不過期
    • 二、做好Redis監控,請求串行化通路(性能較差)
    • 使用mutex鎖機制:就是在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作傳回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作傳回成功時,再進行load db的操作并回設緩存;否則,就重試整個get緩存的方法,代碼實作如下:
public Result get(int ID){
    RedisResult = Redis.get(ID);
     if(RedisResult != null){
         return RedisResult;
     }
     if(Redis.setnx("update:" + ID) != "0"){
         DBResult = DB.selectByID(ID);
         if(DBResult != null){ // 避免緩存穿透
             Redis.set(ID,DBResult);
             Redis.del("update:" + ID);
             return DBResult;
         }
         Redis.set(ID,"Not Data");
         return "抱歉,目前查詢暫時沒有找到資料......";
     }
     Thread.sleep(2);
     return get(ID);
}           

四、Redis八種淘汰政策與三種删除政策

4.1. 八種鍵淘汰(過期)政策

  • 面試官: 你前面提到過,Redis的資料是全部放在記憶體中的,那麼有些資料我也沒有設定過期時間,導緻了大量的記憶體浪費,當我有新的資料需要寫入記憶體不夠用了怎麼辦?
  • 我的内心: 好家夥,問個Redis淘汰政策這麼拐彎抹角.......
  • 我: 我想你是想問記憶體淘汰政策吧,Redis在5.0之前為我們提供了六種淘汰政策,而5.0為我們提供了八種,但是大體上來說這些lru、lfu、random、ttl四種類型,如下:
政策 概述
volatile-lru 從已設定過期時間的資料集中挑選最近最少使用的資料淘汰,沒有設定過期時間的key不會被淘汰,這樣就可以在增加記憶體空間的同時保證需要持久化的資料不會丢失。
volatile-ttl 從已設定過期時間的資料集中挑選将要過期的資料淘汰,ttl值越大越優先被淘汰。
volatile-random 從已設定過期時間的資料集中任意選擇資料淘汰
volatile-lfu 從已設定過期時間的資料集挑選使用頻率最低的資料淘汰
allkeys-lru 從資料集中挑選最近最少使用的資料淘汰,該政策要淘汰的key面向的是全體key集合,而非過期的key集合(應用最廣泛的政策)。
allkeys-lfu 從資料集中挑選使用頻率最低的資料淘汰
allkeys-random 從資料集(server.db[i].dict)中任意選擇資料淘汰
no-enviction(驅逐) 禁止驅逐資料,這也是預設政策。意思是當記憶體不足以容納新入資料時,新寫入操作就會報錯,請求可以繼續進行,線上任務也不能持續進行,采用no-enviction政策可以保證資料不被丢失。
  • 我喘了口氣接着說: 一、在Redis中,資料有一部分通路頻率較高,其餘部分通路頻率較低,或者無法預測資料的使用頻率時,設定allkeys-lru是比較合适的。 二、如果所有資料通路機率大緻相等時,可以選擇allkeys-random。 三、如果研發者需要通過設定不同的ttl來判斷資料過期的先後順序,此時可以選擇volatile-ttl政策。 四、如果希望一些資料能長期被儲存,而一些資料可以被淘汰掉時,選擇volatile-lru或volatile-random都是比較不錯的。 五、由于設定expire會消耗額外的記憶體,如果計劃避免Redis記憶體在此項上的浪費,可以選用allkeys-lru政策,這樣就可以不再設定過期時間,高效利用記憶體了。 maxmemory-policy:參數配置淘汰政策。maxmemory:限制記憶體大小。

4.2. 三種鍵删除政策

  • 面試官: 那Redis的Key删除政策有了解過嗎?
  • 我: Redis删除Key的政策政策有三種: 定時删除:在設定鍵的過期時間的同時,設定一個定時器,當鍵過期了,定時器馬上把該鍵删除。(定時删除對記憶體來說是友好的,因為它可以及時清理過期鍵;但對CPU是不友好的,如果過期鍵太多,删除操作會消耗過多的資源。) 惰性删除:key過期後任然留在記憶體中不做處理,當有請求操作這個key的時候,會檢查這個key是否過期,如果過期則删除,否則傳回key對應的資料資訊。(惰性删除對CPU是友好的,因為隻有在讀取的時候檢測到過期了才會将其删除。但對記憶體是不友好,如果過期鍵後續不被通路,那麼這些過期鍵将積累在緩存中,對記憶體消耗是比較大的。) 定期删除:Redis資料庫預設每隔100ms就會進行随機抽取一些設定過期時間的key進行檢測,過期則删除。(定期删除是定時删除和惰性删除的一個折中方案。可以根據實際場景自定義這個間隔時間,在CPU資源和記憶體資源上作出權衡。) Redis預設采用定期+惰性删除政策。

五、Redis三種持久化機制

5.1. RDB持久化

  • 面試官: 那麼你剛剛提到的Redis為了保證性能會将所有資料放在記憶體,那麼機器突然斷電或當機需要重新開機,記憶體中的資料豈不是沒有了?
  • 我: Redis的确是将資料存儲在記憶體的,但是也會有相關的持久化機制将記憶體持久化備份到磁盤,以便于重新開機時資料能夠重新恢複到記憶體中,避免資料丢失的風險。而Redis持久化機制由三種,在4.X版本之前Redis隻支援AOF以及RDB兩種形式持久化,但是因為AOF與RDB都存在各自的缺陷,是以在4.x版本之後Redis還提供一種新的持久化機制:混合型持久化(但是最終生成的檔案還是.AOF)。
  • 面試官: 那你仔細講講這幾種持久化機制吧
  • 我: 好的,RDB持久化把記憶體中目前程序的資料生成快照(.rdb)檔案儲存到硬碟的過程,有手動觸發和自動觸發: 自動觸發: Redis RDB持久化預設開啟

    save 900 1 -- 900s記憶體在1個寫操作

    save 300 10 -- 300s記憶體在10個寫操作

    save 60 10000 -- 60s記憶體在10000個寫操作

    如上是RDB的自動觸發的預設配置,當操作滿足如上條件時會被觸發。 手動觸發: save:阻塞目前 Redis,直到RDB持久化過程完成為止,若記憶體執行個體比較大 會造成長時間阻塞,線上環境不建議用它 bgsave:Redis 程序執行fork操作建立子程序,由子程序完成持久化,阻塞時 間很短(微秒級),是save的優化,在執行Redis-cli shutdown關閉Redis服務時或執行flushall指令時,如果沒有開啟AOF持久化,自動執行bgsave,bgsave執行流程如下:

與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

而且RDB 是在某個時間點将資料寫入一個臨時檔案,持久化結束後,用這個臨時檔案替換上次持久化的檔案,重新開機時加載這個檔案達到資料恢複。

  • RDB優缺點: 優點:使用單獨子程序來進行持久化,主程序不會進行任何 IO 操作,保證了 Redis 的高性能;而且RDB檔案存儲的是壓縮的二進制檔案,适用于備份、全量複制,可用于災難備份,同時RDB檔案的加載速度遠超于AOF檔案。 缺點:RDB是間隔一段時間進行持久化,如果持久化之間的時間内發生故障,會出現資料丢失。是以這種方式更适合資料要求不嚴謹的時候,因為RDB無法做到實時持久化,而且每次都要建立子程序,頻繁建立成本過高;備份時占用記憶體,因為Redis 在備份時會獨立建立一個子程序,将資料寫入到一個臨時檔案(需要的記憶體是原本的兩倍);還有一點,RDB檔案儲存的二進制檔案存在新老版本不相容的問題。
  • 5.1. AOF持久化

    • 我: 而AOF持久化方式能很好的解決RDB持久化方式造成的資料丢失,AOF持久化到硬碟中的并不是記憶體中的資料快照,而是和MySQL的binlog日志一樣記錄寫入指令,AOF的持久化政策也有三種: appendfsync always:同步持久化形式,每次發生資料更改都将指令追加到AOF檔案,因為每次寫入時都記錄會産生大量磁盤IO,進而性能會受到影響,但是資料最安全。 appendfsync everysec:Redis開啟AOF後的預設配置,異步操作,每秒将寫入指令追加到AOF檔案,如果在剛持久化之後的一秒内當機,會造成1S的資料丢失。 appendfsync no:Redis并不直接調用檔案同步,而是交給作業系統來處理,作業系統可以根據buffer填充情況/通道空閑時間等擇機觸發同步;這是一種普通的檔案操作方式。性能較好,在實體伺服器故障時,資料丢失量會因OS配置有關。 AOF持久化機制優缺點: 優點:根據不同的fsync政策可以保證資料丢失風險降到最低,資料能夠保證是最新的,fsync是背景線程在處理,是以對于處理用戶端請求的線程并不影響。 缺點:檔案體積由于儲存的是所有指令會比RDB大上很多,而且資料恢複時也需要重新執行指令,在重新開機時恢複資料的時間往往會慢很多。雖然fsync并不是共用處理用戶端請求線程的資源來處理的,但是這兩個線程還是在共享同一台機器的資源,是以在高并發場景下也會一定受到影響。
    • AOF機制重寫: 随着Redis線上上運作的時間越來越久,用戶端執行的指令越來越多,AOF的檔案也會越來越大,當AOF達到一定程度大小之後再通過AOF檔案恢複資料是異常緩慢的,那麼對于這種情況Redis在開啟AOF持久化機制的時候會存在AOF檔案的重寫,預設配置是當AOF檔案比上一次重寫時的檔案大小增長100%并且檔案大小不小于64MB時會對整個AOF檔案進行重寫進而達到“減肥”的目的(這裡的100%和64MB可以通過auto-aof-rewrite-percentage 100 與 auto-aof-rewrite-min-size 64mb來調整)。而AOF rewrite操作就是“壓縮”AOF檔案的過程,當然 Redis 并沒有采用“基于原aof檔案”來重寫的方式,而是采取了類似snapshot的方式:基于copy-on-write,全量周遊記憶體中資料,然後逐個序列到aof檔案中。是以AOF rewrite能夠正确反應目前記憶體資料的狀态,這正是我們所需要的;*rewrite過程中,對于新的變更操作将仍然被寫入到原 AOF檔案中,同時這些新的變更操作也會被 Redis 收集起來(buffer,copy-on-write方式下,最極端的可能是所有的key都在此期間被修改,将會耗費2倍記憶體),當記憶體資料被全部寫入到新的aof檔案之後,收集的新的變更操作也将會一并追加到新的aof檔案中,此後将會重命名新的aof檔案為appendonly.aof, 此後所有的操作都将被寫入新的aof檔案。如果在rewrite過程中,出現故障,将不會影響原AOF檔案的正常工作,隻有當rewrite完成之後才會切換檔案,因為rewrite過程是比較可靠的,觸發rewrite的時機可以通過配置檔案來聲明,同時Redis中可以通過bgrewriteaof指令人工幹預。
    • AOF持久化過程如下:
    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    面試官: 那你項目中Redis采用的是那種持久化方式呢?

  • 我: 在我們項目中考慮到了Redis中不僅僅隻是用來做緩存,其中還存儲着一些MySQL中不存在的資料,是以資料的安全性要求比較高,而RDB因為并不是實時的持久化,會出現資料丢失,但是采用AOF形式在重新開機、災備、遷移的時候過程異常耗時,也并不理想,是以在我們線上是同時采用兩種形式的,而AOF+RDB兩種模式同時開啟時Redis重新開機又該加載誰呢?(說着說着我又掏出了紙筆給面試官畫了如下一幅圖):
  • 與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    5.3. 4.x之後的混合型持久化

    當然在Redis4.x之後推出了混合型持久化機制,因為RDB雖然加載快但是存在資料丢失,AOF資料安全但是加載緩慢,Redis為了解決這個問題,帶來了一個新的持久化選項——混合持久化。

    将RDB檔案的内容和增量的AOF日志檔案存在一起。這裡的AOF日志不再是全量 的日志,而是自持久化開始到持久化結束的這段時間發生的增量AOF日志,通常這部分AOF日志很小。Redis重新開機的時候,可以先加載RDB的内容,然後再重放增量AOF日志,就可以完全替代之前的AOF全量檔案重放,恢複效率是以大幅得到提升(混合型持久化最終生成的檔案字尾是.aof,可以通過redis.conf檔案中aof-use-rdb-preamble yes配置開啟)。

    - 混合型持久化優點:結合了RDB和AOF的優點,使得資料恢複的效率大幅提升 - 混合型持久化缺點:相容性不好,Redis-4.x新增,雖然最終的檔案也是.aof格式的檔案,但在4.0之前版本都不識别該aof檔案,同時由于前部分是RDB格式,閱讀性較差

    六、Redis的事務機制

    • 面試官: 既然Redis是資料庫,那麼它支不支援事務呢?
    • 我: Redis作為資料庫當然是支援事務的,隻不過Redis的事務機制是弱事務,相對來說比較雞肋,官方給出如下幾個指令來進行Redis的事務控制: MULTI:标記一個事務塊的開始 DISCARD:取消事務,放棄執行事務塊内的所有指令 EXEC:執行所有事務塊内的指令 UNWATCH:取消WATCH指令對所有key的監視 WATCH key [key ...]:監視一個(或多個)key,如果在事務執行之前這個(或這些)key被其他指令所改動,那麼事務将被打斷

    七、Redis記憶體模型及記憶體劃分

    • 面試官: 嗯嗯,挺不錯,那你對于Redis的記憶體模型以及記憶體的劃分有去了解過嘛?
    • 我: 了解過的,Redis的記憶體模型我們可以通過用戶端連接配接之後使用記憶體統計指令info memory去檢視,如下: used_memory(機關:位元組): Redis配置設定器配置設定的記憶體總量,包括使用的虛拟記憶體(稍後會詳解) used_memory_rss(機關:位元組): Redis程序占據作業系統的記憶體;除了配置設定器配置設定的記憶體之外,used_memory_rss還包括程序運作本身需要的記憶體、記憶體碎片等,但是不包括虛拟記憶體 說明: used_memory是從Redis角度得到的量,used_memory_rss是從作業系統角度得到的量。二者之是以有所不同,一方面是因為記憶體碎片和Redis程序運作需要占用記憶體,使得used_memory_rss可能更大;另一方面虛拟記憶體的存在,使得used_memory可能更大 mem_fragmentation_ratio: 記憶體碎片比率,該值是used_memory_rss / used_memory;一般大于1,且該值越大,記憶體碎片比例越大。而小于1,說明Redis使用了虛拟記憶體,由于虛拟記憶體的媒介是磁盤,比記憶體速度要慢很多,當這種情況出現時,應該及時排查,如果記憶體不足應該及時處理,如增加Redis節點、增加Redis伺服器的記憶體、優化應用等;一般來說,mem_fragmentation_ratio在1.03左右是比較健康的狀态(對于jemalloc配置設定器來說),由于在實際應用中,Redis的資料量會比較大,此時程序運作占用的記憶體與Redis資料量和記憶體碎片相比,都會小得多,mem_fragmentation_ratio便成了衡量Redis記憶體碎片率的參數 mem_allocator: Redis使用的記憶體配置設定器,在編譯時指定;可以是libc 、jemalloc或tcmalloc,預設是jemalloc
    • 我接着說: 而Redis作為記憶體資料庫,在記憶體中存儲的内容主要是資料,但除了資料以外,Redis的其他部分也會占用記憶體。Redis的記憶體占用可以劃分為以下幾個部分: 資料: 作為資料庫,資料是最主要的部分;這部分占用的記憶體會統計在used_memory中 程序本身運作需要的記憶體: Redis主程序本身運作肯定需要占用記憶體,如代碼、常量池等等,這部分記憶體大約幾兆,在大多數生産環境中與Redis資料占用的記憶體相比可以忽略。這部分記憶體不是由jemalloc配置設定,是以不會統計在used_memory中。除了主程序外,Redis建立的子程序運作也會占用記憶體,如Redis執行AOF、RDB重寫時建立的子程序。當然,這部分記憶體不屬于Redis程序,也不會統計在used_memory和used_memory_rss中。 緩沖記憶體: 緩沖記憶體包括用戶端緩沖區、複制積壓緩沖區、AOF緩沖區等;其中,用戶端緩沖存儲用戶端連接配接的輸入輸出緩沖;複制積壓緩沖用于部分複制功能;AOF緩沖區用于在進行AOF重寫時,儲存最近的寫入指令。在了解相應功能之前,不需要知道這些緩沖的細節;這部分記憶體由jemalloc配置設定,是以會統計在used_memory中。 記憶體碎片: 記憶體碎片是Redis在配置設定、回收實體記憶體過程中産生的。例如,如果對資料的更改頻繁,而且資料之間的大小相差很大,可能導緻Redis釋放的空間在實體記憶體中并沒有釋放,但Redis又無法有效利用,這就形成了記憶體碎片。記憶體碎片不會統 計在used_memory中。 記憶體碎片的産生與對資料進行的操作、資料的特點等都有關;此外,與使用的記憶體配置設定器也有關系:如果記憶體配置設定器設計合理,可以盡可能的減少記憶體碎片的産生。如果Redis伺服器中的記憶體碎片已經很大,可以通過安全重新開機的方式減小記憶體碎片:因為重新開機之後,Redis重新從備份檔案中讀取資料,在記憶體中進行重排,為每個資料重新選擇合适的記憶體單元,減小記憶體碎片。
    • 面試官: 那Redis的共享對象你有了解過嗎?
    • 在RedisObject對象中有一個refcount,refcount記錄的是該對象被引用的次數,類型為整型。refcount的作用,主要在于對象的引用計數和記憶體回收。當建立新對象時,refcount初始化為1;當有新程式使用該對象時,refcount加1;當對象不再被一個新程式使用時,refcount減1;當refcount變為0時,對象占用的記憶體會被釋放。 Redis中被多次使用的對象(refcount>1),稱為共享對象。Redis為了節省記憶體,當有一些對象重複出現時,新的程式不會建立新的對象,而是仍然使用原來的對象。這個被重複使用的對象,就是共享對象。目前共享對象僅支援整數值的字元串對象。 共享對象的具體實作:

      Redis的共享對象目前隻支援整數值的字元串對象。之是以如此,實際上是對記憶體和CPU(時間)的平衡:共享對象雖然會降低記憶體消耗,但是判斷兩個對象是否相等卻需要消耗額外的時間。對于整數值,判斷操作複雜度為O(1);對于普通字元串,判斷複雜度為O(n);而對于哈希、清單、集合和有序集合,判斷的複雜度為O(n^2)。 雖然共享對象隻能是整數值的字元串對象,但是5種類型都可能使用共享對象(如哈希、清單等的元素可以使用)。

      就目前的實作來說,Redis伺服器在初始化時,會建立10000個字元串對象,值分别是0-9999的整數值;當Redis需要使用值為0-9999的字元串對象時,可以直接使用這些共享對象。10000這個數字可以通過調整參數Redis_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進行改變。

      共享對象的引用次數可以通過object refcount指令檢視。

    八、Redis虛拟記憶體

    • 面試官: 剛剛聽你提到過Redis的虛拟記憶體,那你能詳細講講它是怎麼會事嗎?
    • 我: 首先說明下Redis的虛拟記憶體與作業系統虛拟記憶體不是一碼事,但是思路和目的都是相冋的。就是暫時把不經常通路的資料從內存交換到磁盤中,進而騰出寶貴的記憶體空間。對于Redis這樣的記憶體資料庫,記憶體總是不夠用的。除了可以将資料分割到多個Redis執行個體以外。另外的能夠提高資料庫容量的辦法就是使用虛拟記憶體技術把那些不經常通路的資料交換到磁盤上。如果我們存儲的資料總是有少部分資料被經常通路,大部分資料很少被通路,對于網站來說确實總是隻有少量使用者經常活躍。當少量資料被經常通路時,使用虛拟記憶體不但能提高單台 Redis資料庫伺服器的容量,而且也不會對性能造成太多影響Redis沒有使用作業系統提供的虛拟記憶體機制而是自己在使用者态實作了自己的虛拟記憶體機制。主要的理由有以下兩點: 一、作業系統的虛拟記憶體是以4k/頁為最小機關進行交換的。而Redis的大多數對象都遠小于4k,是以一個作業系統頁上可能有多個Redis對象。另外 Redis的集合對象類型如list,set可能行在于多個作業系統頁上。最終可能造成隻有10%的key被經常通路,但是所有作業系統頁都會被作業系統認為是活躍的,這樣隻有記憶體真正耗盡時作業系統才會進行頁的交換 二、相比作業系統的交換方式,Redis可以将被交換到磁盤的對象進行壓縮,儲存到磁盤的對象可以去除指針和對象中繼資料資訊。一般壓縮後的對象會比記憶體中的對象小10倍。這樣Redis的虛拟記憶體會比作業系統的虛拟記憶體少做很多I0操作
    • 我: 而關于Redis虛拟記憶體的配置也存在于redis.conf檔案中,如下: vm-enabled ves:#開啟虛拟記憶體功能 vm-swap-file ../redis.swap:#交換出來value儲存的檔案路徑 Vm-max-memory 268435456:# Redis使用的最大記憶體上限(256MB),超過上限後Redis開始交換value到磁盤swap檔案中。建議設定為系統空閑記憶體的60%-80% vm-page-size 32:#每個 Redis頁的大小32個位元組 vm-pages 134217728:#最多在檔案中使用多少個頁,交換檔案的大小 vm-max-threads 8:#用于執行value對象換入換出的工作線程數量,0表示不使用工作線程(詳情後面介紹)。
    • 我: Redis的虛拟記憶體在設計上為了保證key的查詢速度,隻會将value交換到swap檔案。如果是由于太多key很小的value造成的記憶體問題,那麼Redis的虛拟記憶體并不能解決問題。和作業系統一樣 Redis也是按頁來交換對象的。Redis規定同一個頁隻能儲存一個對象。但是一個對象可以儲存在多個頁中。在Redis使用的記憶體沒超過vm-max-memory之前是不會交換任何value的。當超過最大記憶體限制後,Redis會選擇把較老的對象交換到swap檔案中去。如果兩個對象一樣老會優先交換比較大的對象,精确的交換計算公式swappability=age*1og(size_Inmemory)。對于vm-page-size的設定應該根據自己應用将頁的大小設定為可以容納大多數對象的尺寸。太大了會浪費磁盤空間,太小了會造成交換檔案出現過多碎片。對于交換檔案中的每個頁, Redis會在記憶體中用一個1bit值來對應記錄頁的空閑狀态。是以像上面配置中頁數量(vm pages134217728)會占用16MB記憶體用來記錄頁的空內狀态。vm-max-threads表示用做交換任務的工作線程數量。如果大于0推薦設為伺服器的cpu的核心數。如果是0則交換過程在上線程進行。具體工作模式如下: 阻塞模式(vm-max-threads=0): 換出:主線程定期檢査發現記憶體超出最大上限後,會直接以阻塞的方式,将選中的對象儲存到swap檔案中,并釋放對象占用的記憶體空間,此過程會一直重複直到下面條件滿足。 記憶體使用降到最大限制以下 swap檔案滿了 幾乎全部的對象都被交換到磁盤了 換入:當有用戶端請求已經被換出的value時,主線程會以陽塞的方式從swap檔案中加載對應的value對象,加載時此時會阻塞所用戶端。然後處理該用戶端的請求 非阻塞模式(vm-max-threads>0): 換出:當主線程檢測到使用記憶體超過最大上限,會将選中要父換的對象資訊放到一個隊列中父給工作線程背景處理,主線程會繼續處理用戶端請求 換入:如果有用戶端請求的key已終被換出了,主線程會先陽塞發出指令的用戶端,然後将加載對象的資訊放到一個隊列中,讓工作線程去加載。加載完畢後工作線程通知主線程。主線程再執行用戶端的指令。這種方式隻阻塞請求的value是已經被 換出key的用戶端總的來說阻塞方式的性能會好些,因為不需要線程同步、建立線程和恢複被阻塞的用戶端等開銷。但是也相應的犧牡了響應性。工作線稈方式主線程不會陽塞在磁盤1O上,是以響應性更好。如果我們的應用不太經常發生換入換出,而且也不太在意有點延遲的話推薦使用阻塞方式(詳細介紹參考)。

    九、Redis用戶端通信RESP協定

    • 面試官: 那你再簡單講講Redis的用戶端通信的RESP協定吧
    • 我: 這個比較簡單,RESP是Redis序列化協定,Redis用戶端RESP協定與Redis伺服器通信。RESP協定在Redis 1.2中引入,但在Redis 2.0中成為與Redis伺服器通信的标準方式。這個通信方式就是Redis用戶端實作的協定。RESP實際上是一個序列化協定,它支援以下資料類型:簡單字元串、錯誤、整數、大容量字元串和數組。當我們在用戶端中像Redis發送操作指令時,比如:set name 竹子愛熊貓 這條指令,不會直接以這種格式的形式發送到Redis Server,而是經過RESP的序列化之後再發送給Redis執行,而AOF持久化機制持久化之後生成的AOF檔案中也并不是存儲set name 竹子愛熊貓這個指令,而是存儲RESP序列化之後的指令,RESP的特點如下: 實作簡單 能被計算機快速地解析 可讀性好能夠被人工解析

    十、Redis高可用機制:主從複制、哨兵、代理式/分片式叢集

    10.1. 主從複制

    • 面試官: 如果在整個架構中加入Redis作為緩存層,那麼會在Java程式與DB之間多出一層通路,假設Redis挂了那麼Java程式這邊又會抛出異常導緻所有請求死在這裡進而導緻整個系統的不可用,那麼怎麼避免Redis出現這類的單點故障呢?
    • 我: Redis既然這麼受歡迎那麼這些問題它都提供了相關的解決方案的,Redis有提供了主從、哨兵、代理叢集與分片叢集的高可用機制來保證出現單點問題時能夠及時的切換機器以保障整個系統不受到影響。但是後續的三種高可用機制都是基于主從的基礎上來實作的,是以我先說說Redis的主從複制。雖然我們之前講到過持久化機制可以保證資料重新開機情況下也不丢失,但是由于是存在于一台伺服器上的,如果機器磁盤壞了、機房爆炸(玩笑~)等也會導緻資料丢失,而主從複制可以将資料同步到多台不同機器,也能夠保證在主節點當機時任然對外提供服務,還可以做到通過讀寫分離的形式提升整體緩存業務群吞吐量。一般線上上環境時我們去搭建主從環境時,為了保證資料一緻性,從節點是不允許寫的,而是通過複制主節點資料的形式保障資料同步。是以在整個Redis節點群中隻能同時運作存在一台主,其他的全為從節點,示意圖如下(讀的QPS可以通過對從節點的線性擴容來提升):
    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    面試官: 那你能詳細說下主從資料同步的過程嗎?

  • 我: 可以的,Redis2.8之前使用sync[runId][offset]同步指令,Redis2.8之後使用psync[runId][offset]指令。兩者不同在于,sync指令僅支援全量複制過程,psync支援全量和部分複制。介紹同步之前,先介紹幾個概念: runId:每個Redis節點啟動都會生成唯一的uuid,每次Redis重新開機後,runId都會發生變化 offset:主節點和從節點都各自維護自己的主從複制偏移量offset,當主節點有寫入指令時,offset=offset+指令的位元組長度。從節點在收到主節點發送的指令後,也會增加自己的offset,并把自己的offset發送給主節點。這樣,主節點同時儲存自己的offset和從節點的offset,通過對比offset來判斷主從節點資料是否一緻 repl_back_buffer:複制緩沖區,用來存儲增量資料指令 主從資料同步具體過程如下:
  • 與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    我: 當然psync指令除了支援全量複制之外還支援部分複制,因為在做主從資料同步時會導緻主從機器網絡帶寬開銷非常大,而在2.8之前Redis僅支援全量複制,這樣非常容易導緻Redis線上上出現網絡瓶頸,而在2.8之後的增量(部分)複制,用于處理在主從複制中因網絡閃斷等原因造成的資料丢失場景,當slave再次連上master後,如果條件允許,master會補發丢失資料給slave。因為補發的資料遠遠小于全量資料,可以有效避免全量複制的過高開銷。部分複制流程圖如下(複制緩存區溢出也會導緻全量複制):

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!
    • PS: psync[runid][offset]指令三種傳回值: FULLRESYNC:第一次連接配接,進行全量複制 CONTINUE:進行部分複制 ERR:不支援psync指令,進行全量複制
    • 面試官: 那你覺得主從機制有什麼好處?存在什麼問題?
    • 我: 主從機制其實也是為後續的一些高可用機制打下了基礎,但是本身也存在一些缺陷,當然在後續的高可用機制中得到了解決,具體如下: 優點: 能夠為後續的高可用機制打下基礎 在持久化的基礎上能夠将資料同步到其他機器,在極端情況下做到災備的效果 能夠通過主寫從讀的形式實作讀寫分離提升Redis整體吞吐,并且讀的性能可以通過對從節點進行線性擴容無限提升 缺點: 全量資料同步時如果資料量比較大,在之前會導緻線上短暫性的卡頓 一旦主節點當機,從節點晉升為主節點,同時需要修改應用方的主節點位址,還需要指令所有從節點去複制新的主節點,整個過程需要人工幹預 寫入的QPS性能受到主節點限制,雖然主從複制能夠通過讀寫分離來提升整體性能,但是隻有從節點能夠做到線性擴容升吞吐,寫入的性能還是受到主節點限制 木桶效應,整個Redis節點群能夠存儲的資料容量受到所有節點中記憶體最小的那台限制,比如一主兩從架構:master=32GB、slave1=32GB、slave2=16GB,那麼整個Redis節點群能夠存儲的最大容量為16GB

    10.2. 哨兵機制

    • 面試官: 你剛剛提到過後續的高可用機制能解決這些問題,你說的是哨兵嗎?那你再說說哨兵機制
    • 我: 好的,哨兵機制的确能夠解決之前主從存在的一些問題,如圖:
    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    上圖所示是目前企業中常用的Redis架構,一主兩從三哨兵架構,Redis Sentinel(哨兵)主要功能包括主節點存活檢測、主從運作情況檢測、自動故障轉移、主從切換。Redis Sentinel最小配置是一主一從。Redis的Sentinel系統可以用來管理多個Redis節點,該系統可以執行以下四個任務: 監控:不斷檢查主伺服器和從伺服器是否正常運作 通知:當被監控的某個Redis伺服器出現問題,Sentinel通過API腳本向管理者或者其他應用程式發出通知 自動故障轉移:當主節點不能正常工作時,Sentinel會開始一次自動的故障轉移操作,它會将與失效主節點是主從關系的其中一個從節點更新為新的主節點,并且将其他的從節點指向新的主節點,這樣就不需要人工幹預進行主從切換 配置提供者:在Sentinel模式下,用戶端應用在初始化時連接配接的是Sentinel節點集合,從中擷取主節點的資訊

  • 面試官: 那你能講講哨兵機制原理嗎?
  • 我: 可以的,哨兵的工作原理如下: 一、每個哨兵節點每10秒會向主節點和從節點發送info指令擷取最級聯結構圖,哨兵配置時隻要配置對主節點的監控即可,通過向主節點發送info,擷取從節點的資訊,并當有新的從節點加入時可以馬上感覺到
  • 與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    二、每個哨兵節點每隔2秒會向Redis資料節點的指定頻道上發送該哨兵節點對于主節點的判斷以及目前哨兵節點的資訊,同時每個哨兵節點也會訂閱該頻道,來了解其它哨兵節點的資訊及對主節點的判斷,其實就是通過消息publish和subscribe來完成的

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    三、隔1秒每個哨兵根據自己info擷取的級聯結構資訊,會向主節點、從節點及其餘哨兵節點發送一次ping指令做一次心跳檢測,這個也是哨兵用來判斷節點是否正常的重要依據

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    四、Sentinel會以每秒一次的頻率向所有與其建立了指令連接配接的執行個體(master、salve、其他Sentinel)發ping指令,通過判斷ping回複是有效回複還是無效回複來判斷執行個體是否線上/存活(對該Sentinel來說是“主觀線上”),Sentinel配置檔案中的down-after-milliseconds設定了判斷主觀下線的時間長度,如果執行個體在down-after-milliseconds毫秒内,傳回的都是無效回複,那麼Sentinel會認為該執行個體已(主觀)下線,修改其flags狀态為SRI_S_DOWN。如果多個Sentinel監視一個服務,有可能存在多個Sentinel的down-after-milliseconds配置不同,這個在實際生産中要注意(主觀下線:所謂主觀下線,就是單個Sentinel認為某個執行個體下線(有可能是接收不到訂閱,之間的網絡不通等等原因))

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    五、當主觀下線的節點是主節點時,此時該哨兵3節點會通過指令sentinel is-masterdown-by-addr尋求其它哨兵節點對主節點的判斷,如果其他的哨兵也認為主節點主觀下線了,則當認為主觀下線的票數超過了quorum(選舉)個數,此時哨兵節點則認為該主節點确實有問題,這樣就客觀下線了,大部分哨兵節點都同意下線操作,也就說是客觀下線

    一般情況下,每個Sentinel會以每10秒一次的頻率向它已知的所有主伺服器和從伺服器發送INFO指令,當一個主伺服器被标記為客觀下線時,Sentinel向下線主伺服器的所有從伺服器發送INFO指令的頻率,會從10秒一次改為每秒一次

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    六、Sentinel和其他Sentinel協商客觀下線的主節點的狀态,如果處于SDOWN狀态,則自動選出新的主節點,将剩餘從節點指向新的主節點進行資料複制

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!
    • 新主選舉原理(自動故障轉移):Sentinel狀态資料結構中儲存了主服務的所有從服務資訊,領頭Sentinel按照如下的規則從從服務清單中挑選出新的主服務: 過濾掉主觀下線的節點 選擇slave-priority最高的節點,如果有則傳回沒有就繼續選擇 選擇出複制偏移量最大的系節點,因為複制便宜量越大則資料複制的越完整,如果有就傳回了,沒有就繼續下一步 選擇run_id最小的節點 通過slaveof no one指令,讓選出來的從節點成為主節點;并通過slaveof指令讓其他節點成為其從節點 将已下線的主節點設定成新的主節點的從節點,當其回複正常時,複制新的主節點,變成新的主節點的從節點,同理,當已下線的服務重新上線時,Sentinel會向其發送slaveof指令,讓其成為新主的從 哨兵lerder選舉流程:如果主節點被判定為客觀下線之後,就要選取一個哨兵節點來完成後面的故障轉移工作,選舉出一個leader的流程如下: 每個線上的哨兵節點都可以成為上司者,當它确認主節點下線時,會向其它哨兵發is-master-down-by-addr指令,征求判斷并要求将自己設定為上司者,由上司者處理故障轉移 當其它哨兵收到此指令時,可以同意或者拒絕它成為上司者 如果征求投票的哨兵發現自己在選舉的票數大于等于num(sentinels)/2+1時,将成為上司者,如果沒有超過,繼續重複選舉………… 服務下線注意事項: 主觀下線:單個哨兵節點認為某個節點故障時出現的情況,一般出現主觀下線的節點為從節點時,不需要與其他哨兵協商,目前哨兵可直接對改節點完成下線操作 客觀下線:當一個節點被哨兵判定為主觀下線時,這個節點是主節點,那麼會和其他哨兵協商完成下線操作的情況被稱為客觀下線(客觀下線隻存在于主節點)
    • 面試官: 那你覺得哨兵真正的實作了高可用嗎?或者說你認為哨兵機制完美了嘛?
    • 我: 剛剛在之前我提到過,哨兵解決了之前主從存在的一些問題,具體如下: 哨兵機制優點: 解決了之前主從切換需要人工幹預問題,保證了一定意義上的高可用 哨兵機制缺點: 全量資料同步仍然會導緻線上出現短暫卡頓 寫入QPS仍然受到主節點單機限制,對于寫入并發較高的項目無法滿足需求 仍然存在主從複制時的木桶效應問題,存儲容量受到節點群中最小記憶體機器限制

    10.3. 代理式叢集

    • 面試官: 嗯嗯,對于類似于淘寶、新浪微網誌之類的網際網路項目,那麼怎麼做到真正意義上的高可用呢?
    • 我: 之前的哨兵并不算真正意義上的叢集,隻解決了人工切換問題,如果需要大規模的寫入支援,或者緩存資料量巨大的情況下隻能夠通過加機器記憶體的形式來解決,但是長此已久并不是一個好的方案,而在Redis3.0之前官方卻并沒有相對應的解決方案,不過在Redis3.0之前卻有很多其他的解決方案的提出以及落地,比如: TwemProxy:TwemProxy是一種代理分片機制,由Twitter開源。Twemproxy作為代理, 可接受來自多個程式的通路,按照路由規則,轉發給背景的各個Redis伺服器,再原路傳回。這個方案順理成章地解決了單個Redis執行個體承載能力的問題。當然,Twemproxy本身也是單點,需要用Keepalived做高可用方案。這麼些年來,Twemproxy是應用範圍最廣、穩定性最高、 最久經考驗的分布式中間件。隻是,他還有諸多不友善之處。Twemproxy最大的痛點在于,無法平滑地擴容/縮容。這樣增加了運維難度:業務量突增,需增加Redis伺服器; 業務量菱縮,需要減少Redis伺服器。但對Twemproxy而言,基本上都很難操作。或者說,Twemproxy更加像伺服器端靜态sharding,有時為了規避業務量突增導緻的擴容需求,甚至被迫新開一個基于Twemproxy的Redis叢集。Twemproxy另一個痛點是,運維不友好,甚至沒有控制台。當然,由于使用了中間件代理,相比用戶端直接連伺服器方式,性能上有所損耗,實測結果降低20%左右。
    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    Codis:Codis由豌豆英于2014年11月開源,基于Go、C開發,是近期湧現的、國人開發的優秀開源軟體之一。現已廣泛用于豌豆英的各種Redis業務場景,從各種壓力測試來看,穩定性符合高效運維的要求。性能更是改善很多,最初比Twemproxy慢20%;現在比Twemproxy快近100% (條件:多執行個體,-般Value長度)。

    Codis具有可視化運維管理界面。Codis無疑是 為解決Twemproxy缺點而出的新解決方案。是以綜合方面會優于Twemproxy很多。目前也越來越多公司選擇Codis,Codis引入了Group的概念,每個Group包括1個Master及至少1個Slave,這是和Twemproxy的差別之一。這樣做的好處是,如果目前Master有問題,則運維人員可通過Dashboard“自助式”切換到Slave,而不需要小心翼翼地修改程式配置檔案。

    為支援資料熱遷移(AutoRebalance),出品方修改了RedisServer源碼,并稱之為Codis Server,Codis采用預先分片(Pre-Sharding)機制,事先規定好了,分成1024個slots (也就是說,最多能支援後端1024個CodisServer),這些路由資訊儲存在ZooKeeper中。 不足之處有對Redis源碼進行了修改,以及代理實作本身會有的問題。

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    我: 實則代理分片的原理也很簡單,類似于代理式的分庫分表的實作,之前我們是直接連接配接Redis,然後對Redis進行讀寫操作,現在則是連接配接代理,讀寫操作全部交由代理來處理分發到具體的Redis執行個體,而叢集的組成就很好的打破了之前的一主多從架構,形成了多主多從的模式,每個節點由一個個主從來建構,每個節點存儲不同的資料,每個節點都能夠提供讀寫服務,進而做到真正意義上的高可用,具體結構如下:

    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!
    • 面試官: 嗯,那麼為什麼現在一般公司在考慮技術選型的時候為什麼不考慮這兩種方案呢?
    • 我: 因為使用代理之後能夠去解決哨兵存在的問題,但是凡事有利必有弊,代理式叢集具體情況如下: 優點: 打破了傳統的一主多從模型,允許多主存在,寫入QPS不再受到單機限制 資料分片存儲,每個節點存儲的資料都不同,解決之前主從架構存在的存容問題 每個節點都是獨立的主從,資料同步并不是真正的“全量”,每個節點同步資料時都隻是同步該節點上master負責的一部分資料 缺點: 由于使用了代理層來打破之前的架構模型,代理層需要承擔所有工作 代理層需要維護,保證高可用 代理層需要實作服務動态感覺、注冊與監聽 代理層需要承載所有用戶端流量 代理層需要處理所有分發請求 由于資料并不存在與同一台機器,Redis的很多指令不再完美支援,如set的交集、并集、差集等

    10.4. 去中心化分片式叢集

    • 面試官: 那麼既然代理分片式的叢集存在這麼多需要考慮解決的問題,現在如果讓你做架設,做技術選型你會考慮哪種方案呢?
    • 我: 我會考慮Redis3.x之後的Redis-cluster去中心化分片式叢集,Redis-cluster在Redis3.0中推出,支援Redis分布式叢集部署模式。采用無中心分布式架構。所有的Redis節點彼此互聯(PING-PONG機制),内部使用二進制協定優化傳輸速度和帶寬節點的fail是通過叢集中超過半數的節點檢測失效時才生效.用戶端與Redis節點直連,不需要中間proxy層.用戶端不需要連接配接叢集所有節點連接配接叢集中任何一個可用節點即可,減少了代理層,大大提高了性能。Redis-cluster把所有的實體節點映射到[0-16383]slot上,cluster負責維護node <-> slot <-> key之間的關系。目前Jedis已經支援Redis-cluster。從計算架構或者性能方面無疑Redis-cluster是最佳的選擇方案。
    • 面試官: 那你能講講Redis-cluster叢集的原理嗎?
    • 我: Redis Cluster在設計中沒有使用一緻性哈希(ConsistencyHashing),而是使用資料分片(Sharding)引入哈希槽(hashSlot)來實作;一個RedisCluster包含16384(0~16383)個哈希槽,存儲在RedisCluster中的所有鍵都會被映射到這些slot中,叢集中的每個鍵都屬于這16384個哈希槽中的一個,叢集使用公式slot=CRC16(key)% 16384來計算key屬于哪個槽,其中CRC16(key)語句用于計算key的CRC16校驗和。 叢集中的每個主節點(Master)都負責處理16384個哈希槽中的一部分,當叢集處于穩定狀态時,每個哈希槽都隻由一個主節點進行處理,每個主節點可以有一個到N個從節點(Slave),當主節點出現當機或網絡斷線等不可用時,從節點能自動提升為主節點進行處理。
    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!
    • 假設我此時向Redis發送一條指令:set name 竹子愛熊貓,那麼Redis會使用CRC16算法計算KEY值,CRC16(name),類似于一個HASH函數,完成後會得到一個數字,假設此時計算完name後得到的結果是26384,那麼會拿着這個計算完成之後的結果%總槽數,26384%16384得到結果為10000,那麼key=name的這個值應該被放入負責10000這個HashSlot存儲,如上圖中,會被放入到第三個節點存儲,當再次get這個緩存時同理(Redis底層的GossIP原理由于本篇篇幅過長則不再闡述)。

    十一、Redis版本新特性

    • 面試官: 既然你在前面提到過這麼多版本之間都有不同的變化,那麼我最後考考你Redis不同的版本之間有什麼差別吧
    • 我: (心想:這不就是考我新特性嗎,Redis問這麼久我都扛不住了,嗓子都冒煙了)好的好的,具體如下:
      • Redis3.x: 支援叢集 在2.6基礎上再次加大原子性指令支援
      • Redis4.x: 主從資料同步機制:4.0之前僅支援pync1,4.x之後支援psync2 線程DEL/FLUSH優化,新的UNLINK與DEL作用相同,FLUSHALL/FLUSHDB中添加了ASYNC選項,Redis現在可以在不同的線程中删除背景的key而不會阻塞伺服器 慢日志記錄用戶端來源IP位址,這個小功能對于故障排查很有用處 混合RDB + AOF格式 新的管理指令: MEMORY:能夠執行不同類型的記憶體分析:記憶體問題的故障排除(使用MEMORY DOCTOR,類似于LATENCYDOCTOR),報告單個鍵使用的記憶體量,更深入地報告Redis記憶體使用情況 SWAPDB:能夠完全立即(無延遲)替換同執行個體下的兩個Redis資料庫(目前我們業務沒啥用) 記憶體使用和性能改進: Redis現在使用更少的記憶體來存儲相同數量的資料 Redis現在可以對使用的記憶體進行碎片整理,并逐漸回收空間
      • Redis5.x: 新的流資料類型(Stream data type) 新的 Redis 子產品API:定時器、叢集和字典API RDB可存儲LFU和LRU資訊 Redis-cli中的叢集管理器從Ruby (redis-trib.rb)移植到了C語言代碼。執行redis-cli --cluster help指令以了解更多資訊 新的有序集合(sorted set)指令:ZPOPMIN/MAX和阻塞變體(blocking variants) 更新Active defragmentation至v2版本 增強HyperLogLog的實作 更好的記憶體統計報告 許多包含子指令的指令現在都有一個HELP子指令 優化用戶端頻繁連接配接和斷開連接配接時,使性能表現更好 更新Jemalloc至5.1版本 引入CLIENT UNBLOCK和CLIENT ID 新增LOLWUT指令 在不存在需要保持向後相容性的地方,棄用"master/slave"術語 網絡層中的差異優化 Lua相關的改進: 将Lua腳本更好地傳播到replicas / AOF Lua腳本現在可以逾時并在副本中進入-BUSY狀态 引入動态的HZ(Dynamic HZ)以平衡空閑CPU使用率和響應性 對Redis核心代碼進行了重構并在許多方面進行了改進,許多錯誤修複和其他方面的改進
      • Redis6.x: ACL:在Redis 5版本之前,Redis安全規則隻有密碼控制還有通過rename來調整高危指令比如flushdb/KEYS*/shutdown等。Redis6則提供ACL的功能對使用者進行更細粒度的權限控制: 接入權限:使用者名和密碼 可以執行的指令 可以操作的KEY 新的Redis通信協定:RESP3 Client side caching用戶端緩存:基于RESP3協定實作的用戶端緩存功能。為了進一步提升緩存的性能,将用戶端經常通路的資料cache到用戶端。減少TCP網絡互動,提升RT IO多線程:O多線程其實指用戶端互動部分的網絡IO互動處理子產品多線程,而非執行指令多線程。作者不想将執行指令多線程是因為要避免複雜性、鎖的效率低下等等。此次支援IO多線程的設計大體如下:
    與面試官徹夜長談Redis緩存、持久化、淘汰機制、叢集底層原理!

    工具支援Cluster叢集:Redis6.0版本後redis/src目錄下提供的大部分工具開始支援Cluster叢集 Modules API:Redis 6中子產品API開發進展非常大,因為Redis Labs為了開發複雜的功能,從一開始就用上Redis子產品。Redis可以變成一個架構,利用Modules來建構不同系統,而不需要從頭開始寫然後還要BSD許可。Redis一開始就是一個向編寫各種系統開放的平台 Disque:Disque作為一個RedisModule使用足以展示Redis的子產品系統的強大。叢集消息總線API、屏蔽和回複用戶端、計時器、子產品資料的AOF和RDB等等

  • 面試官: 嗯嗯,小夥子你前途無量呀,今天晚上友善入職嗎?
  • 我: ..........
  • 繼續閱讀