1. 程式人生 > >Java面試準備-快取使用

Java面試準備-快取使用

問題連結轉載  Java面試通關要點彙總集【終極版】

一、Redis 有哪些型別

  • string型別:string為最簡單型別,一個key對應一個value
set mykey "wangzai"   ##設定key,如果該key存在會被覆蓋
setnx mykey "wangzai" ##如果mykey存在則不改變,如果不存在則建立賦值
get mykey             ##獲取key值
setex key1 10 1       ##給key1設定過期時間為10s,值為1
mset key1 value1 key2 value2    ##設定多個key
mget key1 key2        ##獲取多個key值 
  • list型別:list是一個連結串列結構,主要功能是push,pop以及獲取一個範圍的所有值等。使用list結構可以輕鬆實現最新訊息排行,另一個應用是訊息佇列,可以利用list的push操作,將任務存在list中,然後工作執行緒再用pop操作將任務取出進行執行(先進後出)
lpush list1 "wangzai"       ##在列表中加入一個元素
lrange list1 0 -1           ##檢視list1裡面的所有元素
lpop list1                  ##取出list1最新的元素
linsert list1 before "wangzai" "doubi"     ##在值為"wangzai"的前面插入一個元素為"doubi"
lset list1 3 "hehe"         ##把第五個元素修改為"hehe"
lindex list1 0              ##檢視第一個元素
llen list1                  ##檢視列表中有多少個元素
  • set型別:set是集合,對集合操作有新增刪除元素,有對多個集合求交併差等操作。在微博應用中,可以將一個使用者關注的所有人放在一個集合裡,將所有粉絲放在一個集合裡,因為redis為集合提供了求交集,並集,差集等操作,就可以方便地實現如共同關注,共同喜好等功能
sadd set1 a b c d         ##建立集合set1並設定值
smembers set1             ##檢視集合set1的值
srem set1 a b             ##刪除set1的值
spop set1                 ##隨機取出一個元素並刪除
sinter set1 set2          ##交集
sinterstore set1 set2 set3   ##把交集儲存到set3
sunion set1 set3          ##並集
sunionstore set1 set2 set3     ##把並集儲存到set3
sdiff set1 set2           ##差集
sdiffstore set1 set2 set3  ##把差集儲存到set3
sismember set1 c           ##判斷元素c是否屬於集合set1
srandmember set1           ##隨機取出一個元素,但不刪除
  • sorted set型別:sorted set是有序集合,比set多一個權重引數score,使得集合元素能夠按score進行有序排列
##儲存一個班級同學的成績,其集合value可以是學員學號,而score是考試得分

zadd zset1 1 a      ##增加一個集合zset1 , score為1,member為a
zrange zset1 0 -1   ##按score升序輸出member
zrange zset1 0 -1 withscores   ##帶上score
zrem zset1 a        ##刪除指定元素
zrank zset1 a       ##返回元素的索引值,索引從0開始
zreverange zset1 0 -1   ##score降序輸出member
zcard zset1         ##返回集合中所有元素的個數
zcount zset1 1 10   ##返回分值範圍1 - 10的元素個數
zrangebyscore zset1 1 10   ##返回分值範圍1-10的元素
zremrangebyscore zset1 1 10   ##刪除分值範圍1-10的元素
  • hash型別:把一些結構化的資訊打包成hashmap
hset hash1 name wangzai        ##建立hash(hset name key value)
hget hash1 name                ##獲取field值 HGET name key
hgetall hash1                  ##獲取hash1中所有key和value
hmset hash2 name wangzai age 26 job it    ##批量建立鍵值對
hmget hash2 name age job       ##批量獲取field值
hdel hash2 job                 ##刪除指定field
hkeys hash2                    ##列印所有的key
hvals hash2                    ##列印所有的value
hlen hash2                     ##檢視hash2有幾個field

二、Redis 內部結構

各功能模組說明如下

  • File Event:處理檔案事件(在多個客戶端中實現多路複用,接受它們發來的命令請求),即讀事件;並將命令的執行結果返回給客戶端,即寫事件
  • Time Event:時間事件(更新統計資訊,清理過期資料,附屬節點同步,定期持久化等)
  • AOF:命令日誌的資料持久化
  • RDB:實際的資料持久化
  • Lua Environment:Lua指令碼的執行環境,為了讓Lua環境符合Redis指令碼功能的需求,Redis對Lua環境進行了一系列的修改,包括修改函式庫,更換隨機函式,保護全域性變數等等
  • Command table(命令表):在執行命令時,根據字元來查詢相應命令的實現函式
  • Share Objects(物件共享):

主要儲存常見的值:

  1. 各種命令常見的返回值,例如返回值OK,ERROR,WRONGTYPE等字元
  2. 小於redis.h/REDIS_SHARED_INTEGERS(預設1000)的所有整數。通過預分配的一些常見的值物件,並在多個數據結構間共享物件,即這些常見的值在記憶體中只有一份
  • Databases:Redis資料庫是真正儲存資料的地方。當然資料庫本身也是儲存在記憶體中。Databases的資料結構虛擬碼如下
<span style="font-family:Microsoft YaHei;font-size:18px;">
typedef struct redisDb{

    //儲存著資料庫以整數表示的號碼
    int id;

    //儲存著資料庫中的所有鍵值對資料
    //這個屬性也被稱為鍵空間
    dict *dict

    //儲存著鍵的過期資訊
    dict *expires;
    
    //實現列表阻塞原語,如BLPOP
    dict *blocking_keys;
    dict *ready_keys;

    //用於實現WATCH命令
    dict *watched_keys;
} redisDb;
</span>

Database的內容要點包括:

  1. 資料庫主要由dict和expires兩個字典構成,其中dict儲存鍵值對,而expires則儲存鍵的過期時間
  2. 資料庫的鍵總是一個字串物件,而值可以是任意一種Redis資料型別,包括字串,雜湊,集合,列表和有序集
  3. expires的某個鍵和dict的某個鍵共同指向同一個字串物件,而expires鍵的值則是該鍵以毫秒計算的UNIX過期時間戳
  4. Redis使用惰性刪除和定期刪除兩種策略來刪除過期的鍵
  5. 更新後的RDB檔案和重寫後的AOF檔案都不會保留過期的鍵
  6. 當一個過期鍵被刪除後,程式會追加一條新的DEL命令到現有AOF檔案末尾
  7. 當主節點刪除一個過期鍵後,它會顯式地傳送一條DEL命令到所有附屬節點
  8. 附屬節點即使發現過期鍵,也不會主動刪除它,而是等待主節點發來DEL命令,這樣保持主節點和附屬節點的資料一致

資料庫的dict字典和expires字典的擴充套件策略和普通字典一樣,它們的收縮策略是:當節點的填充百分比不足10%時,將可用節點數量減少至大於等於當前已用節點數量。

三、Redis 記憶體淘汰機制

redis記憶體資料集大小上升到一定大小時就會進行資料淘汰策略

  • 如何配置

通過配置redis.conf中的maxmemory這個值來開啟記憶體淘汰功能

#maxmemory

值得注意的是,maxmemory為0時表示我們對Redis的記憶體使用沒有限制

根據應用場景,選擇淘汰策略

# maxmemory-policy noeviction

記憶體淘汰過程:

  1. 首先,客戶端發起了需要申請更多記憶體的命令(如set)
  2. 然後,Redis檢查記憶體使用情況,如果已使用的記憶體大於maxmemory則開始根據使用者配置的不同淘汰策略來淘汰記憶體(key),從而換取一定的記憶體
  3. 最後,這個命令執行成功
  • 動態配置命令

此外,redis支援動態改配置,無需重啟

#設定最大記憶體
config set maxmemory 100000

#設定淘汰策略
config set maxmemory-policy noeviction
  • 記憶體淘汰策略

volatile-lru

從已設定過期時間的資料集中挑選最近最少使用的資料淘汰。redis並不是保證取得所有資料集中最近最少使用的鍵值對,而只是隨機挑選的幾個鍵值對中的, 當記憶體達到限制的時候無法寫入非過期時間的資料集。

volatile-ttl

從已設定過期時間的資料集中挑選將要過期的資料淘汰。redis 並不是保證取得所有資料集中最近將要過期的鍵值對,而只是隨機挑選的幾個鍵值對中的, 當記憶體達到限制的時候無法寫入非過期時間的資料集。

volatile-random

從已設定過期時間的資料集中任意選擇資料淘汰。當記憶體達到限制的時候無法寫入非過期時間的資料集。

allkeys-lru

從資料集中挑選最近最少使用的資料淘汰。當記憶體達到限制的時候,對所有資料集挑選最近最少使用的資料淘汰,可寫入新的資料集。

allkeys-random

從資料集中任意選擇資料淘汰,當記憶體達到限制的時候,對所有資料集挑選隨機淘汰,可寫入新的資料集。

no-enviction

當記憶體達到限制的時候,不淘汰任何資料,不可寫入任何資料集,所有引起申請記憶體的命令會報錯。

  • 如何選擇淘汰策略

下面看看幾種策略的適用場景

allkeys-lru:如果我們的應用對快取的訪問符合冪律分佈,也就是存在相對熱點資料,或者我們不太清楚我們應用的快取訪問分佈狀況,我們可以選擇allkeys-lru策略。

allkeys-random:如果我們的應用對於快取key的訪問概率相等,則可以使用這個策略。

volatile-ttl:這種策略使得我們可以向Redis提示哪些key更適合被eviction。

另外,volatile-lru策略和volatile-random策略適合我們將一個Redis例項既應用於快取和又應用於持久化儲存的時候,然而我們也可以通過使用兩個Redis例項來達到相同的效果,值得一提的是將key設定過期時間實際上會消耗更多的記憶體,因此我們建議使用allkeys-lru策略從而更有效率的使用記憶體。

四、聊聊 Redis 使用場景

隨著資料量的增長,MySQL 已經滿足不了大型網際網路類應用的需求。因此,Redis 基於記憶體儲存資料,可以極大的提高查詢效能,對產品在架構上很好的補充。在某些場景下,可以充分的利用 Redis 的特性,大大提高效率。

快取

對於熱點資料,快取以後可能讀取數十萬次,因此,對於熱點資料,快取的價值非常大。例如,分類欄目更新頻率不高,但是絕大多數的頁面都需要訪問這個資料,因此讀取頻率相當高,可以考慮基於 Redis 實現快取。

會話快取

此外,還可以考慮使用 Redis 進行會話快取。例如,將 web session 存放在 Redis 中。

時效性

例如驗證碼只有60秒有效期,超過時間無法使用,或者基於 Oauth2 的 Token 只能在 5 分鐘內使用一次,超過時間也無法使用。

訪問頻率

出於減輕伺服器的壓力或防止惡意的洪水攻擊的考慮,需要控制訪問頻率,例如限制 IP 在一段時間的最大訪問量。

計數器

資料統計的需求非常普遍,通過原子遞增保持計數。例如,應用數、資源數、點贊數、收藏數、分享數等。

社交列表

社交屬性相關的列表資訊,例如,使用者點贊列表、使用者分享列表、使用者收藏列表、使用者關注列表、使用者粉絲列表等,使用 Hash 型別資料結構是個不錯的選擇。

記錄使用者判定資訊

記錄使用者判定資訊的需求也非常普遍,可以知道一個使用者是否進行了某個操作。例如,使用者是否點贊、使用者是否收藏、使用者是否分享等。

交集、並集和差集

在某些場景中,例如社交場景,通過交集、並集和差集運算,可以非常方便地實現共同好友,共同關注,共同偏好等社交關係。

熱門列表與排行榜

按照得分進行排序,例如,展示最熱、點選率最高、活躍度最高等條件的排名列表。

最新動態

按照時間順序排列的最新動態,也是一個很好的應用,可以使用 Sorted Set 型別的分數權重儲存 Unix 時間戳進行排序。

訊息佇列

Redis 能作為一個很好的訊息佇列來使用,依賴 List 型別利用 LPUSH 命令將資料新增到連結串列頭部,通過 BRPOP 命令將元素從連結串列尾部取出。同時,市面上成熟的訊息佇列產品有很多,例如 RabbitMQ。因此,更加建議使用 RabbitMQ 作為訊息中介軟體。

五、Redis 持久化機制

Redis是一個支援持久化的記憶體資料庫,通過持久化機制把記憶體中的資料同步到硬碟檔案來保證資料持久化。當Redis重啟後通過把硬碟檔案重新載入到記憶體,就能達到恢復資料的目的。

RDB

RDB是Redis預設的持久化方式。按照一定的時間週期策略把記憶體的資料以快照的形式儲存到硬碟的二進位制檔案。即Snapshot快照儲存,對應產生的資料檔案為dump.rdb,通過配置檔案中的save引數來定義快照的週期。

  1. # 快照的檔名
  2. dbfilename dump.rdb
  3. # 存放快照的目錄
  4. dir /var/lib/redis
  5. # 在進行映象備份時,是否進行壓縮。
  6. # yes:壓縮,但是需要一些cpu的消耗。
  7. # no:不壓縮,需要更多的磁碟空間。
  8. rdbcompression yes
  9. #900秒後且至少1個key發生變化時建立快照
  10. save 900 1
  11. #300秒後且至少10個key發生變化時建立快照
  12. save 300 10
  13. #60秒後且至少10000個key發生變化時建立快照
  14. save 60 10000

一旦資料庫出現問題,那麼我們的RDB檔案中儲存的資料並不是全新的,從上次RDB檔案生成到Redis停機這段時間的資料全部丟掉了。例如,每隔5分鐘或者更長的時間來建立一次快照,Redis停止工作時(例如意外斷電)就可能丟失最近幾分鐘的資料。

AOF

Redis會將每一個收到的寫命令都通過Write函式追加到檔案最後,類似於MySQL的binlog。當Redis重啟是會通過重新執行檔案中儲存的寫命令來在記憶體中重建整個資料庫的內容。

  1. # 是否開啟AOF,預設關閉(no)
  2. appendonly yes

由於Linux會把對檔案的寫入操作通過buffer緩衝,因此Linux可能不是立即寫入到檔案,有對視資料的風險。Redis有三種不同的fsync策略供選擇:no fsync at all、 fsync every second、 fsync at every query。預設為fsync every second此時的寫效能仍然很好,且最壞的情況下可能丟失一秒鐘的寫操作。

  1. # Redis支援三種不同的刷寫模式:
  2. #每次收到寫命令就立即強制寫入磁碟,是最有保證的完全的持久化,但速度也是最慢的,一般不推薦使用。
  3. # appendfsync always
  4. #每秒鐘強制寫入磁碟一次,在效能和持久化方面做了很好的折中,是受推薦的方式。
  5. appendfsync everysec
  6. #完全依賴OS的寫入,一般為30秒左右一次,效能最好但是持久化最沒有保證,不被推薦。
  7. # appendfsync no

AOF帶來了另一個問題,持久化檔案會變得越來越大。比如,我們呼叫INCR test命令100次,檔案中就必須儲存全部的100條命令,但其實99條都是多餘的。因為要恢復資料庫的狀態其實檔案中儲存一條SET test 100就夠了。為了合併重寫AOF的持久化檔案,Redis提供了bgrewriteaof命令。收到此命令後,Redis將使用與快照類似的方式將記憶體中的資料以命令的方式儲存到臨時檔案中,最後替換原來的檔案,以此來實現控制AOF檔案的合併重寫。由於是模擬快照的過程,因此在重寫AOF檔案時並沒有讀取舊的AOF檔案,而是將整個記憶體中的資料庫內容用命令的方式重寫了一個新的AOF檔案。

  1. # AOF檔名
  2. appendfilename appendonly.aof
  3. #當程序中BGSAVE或BGREWRITEAOF命令正在執行時不阻止主程序中的fsync()呼叫(預設為no,當存在延遲問題時需調整為yes)
  4. no-appendfsync-on-rewrite no
  5. #當AOF增長率為100%且達到了64mb時開始自動重寫AOF
  6. auto-aof-rewrite-percentage 100
  7. auto-aof-rewrite-min-size 64mb

六、Redis 叢集方案與實現

下面介紹Redis的叢集方案。

 

Replication(主從複製)

Redis的replication機制允許slave從master那裡通過網路傳輸拷貝到完整的資料備份,從而達到主從機制。為了實現主從複製,我們準備三個redis服務,依次命名為master,slave1,slave2。

配置主伺服器

為了測試效果,我們先修改主伺服器的配置檔案redis.conf的埠資訊

  1. port 6300

配置從伺服器

replication相關的配置比較簡單,只需要把下面一行加到slave的配置檔案中。你只需要把ip地址和埠號改一下。

  1. slaveof 192.168.1.1 6379

我們先修改從伺服器1的配置檔案redis.conf的埠資訊和從伺服器配置。

  1. port 6301
  2. slaveof 127.0.0.1 6300

我們再修改從伺服器2的配置檔案redis.conf的埠資訊和從伺服器配置。

  1. port 6302
  2. slaveof 127.0.0.1 6300

值得注意的是,從redis2.6版本開始,slave支援只讀模式,而且是預設的。可以通過配置項slave-read-only來進行配置。
此外,如果master通過requirepass配置項設定了密碼,slave每次同步操作都需要驗證密碼,可以通過在slave的配置檔案中新增以下配置項

  1. masterauth <password>

測試

分別啟動主伺服器,從伺服器,我們來驗證下主從複製。我們在主伺服器寫入一條訊息,然後再其他從伺服器檢視是否成功複製了。

Sentinel(哨兵)

主從機制,上面的方案中主伺服器可能存在單點故障,萬一主伺服器宕機,這是個麻煩事情,所以Redis提供了Redis-Sentinel,以此來實現主從切換的功能,類似與zookeeper。

Redis-Sentinel是Redis官方推薦的高可用性(HA)解決方案,當用Redis做master-slave的高可用方案時,假如master宕機了,Redis本身(包括它的很多客戶端)都沒有實現自動進行主備切換,而Redis-Sentinel本身也是一個獨立執行的程序,它能監控多個master-slave叢集,發現master宕機後能進行自動切換。

它的主要功能有以下幾點

  • 監控(Monitoring):不斷地檢查redis的主伺服器和從伺服器是否運作正常。
  • 提醒(Notification):如果發現某個redis伺服器執行出現狀況,可以通過 API 向管理員或者其他應用程式傳送通知。
  • 自動故障遷移(Automatic failover):能夠進行自動切換。當一個主伺服器不能正常工作時,會將失效主伺服器的其中一個從伺服器升級為新的主伺服器,並讓失效主伺服器的其他從伺服器改為複製新的主伺服器; 當客戶端試圖連線失效的主伺服器時, 叢集也會向客戶端返回新主伺服器的地址, 使得叢集可以使用新主伺服器代替失效伺服器。

Redis Sentinel 相容 Redis 2.4.16 或以上版本, 推薦使用 Redis 2.8.0 或以上的版本。

配置Sentinel

必須指定一個sentinel的配置檔案sentinel.conf,如果不指定將無法啟動sentinel。首先,我們先建立一個配置檔案sentinel.conf

  1. port 26379
  2. sentinel monitor mymaster 127.0.0.1 6300 2

官方典型的配置如下

  1. sentinel monitor mymaster 127.0.0.1 6379 2
  2. sentinel down-after-milliseconds mymaster 60000
  3. sentinel failover-timeout mymaster 180000
  4. sentinel parallel-syncs mymaster 1
  5.  
  6. sentinel monitor resque 192.168.1.3 6380 4
  7. sentinel down-after-milliseconds resque 10000
  8. sentinel failover-timeout resque 180000
  9. sentinel parallel-syncs resque 5

配置檔案只需要配置master的資訊就好啦,不用配置slave的資訊,因為slave能夠被自動檢測到(master節點會有關於slave的訊息)。

需要注意的是,配置檔案在sentinel執行期間是會被動態修改的,例如當發生主備切換時候,配置檔案中的master會被修改為另外一個slave。這樣,之後sentinel如果重啟時,就可以根據這個配置來恢復其之前所監控的redis叢集的狀態。

接下來我們將一行一行地解釋上面的配置項:

  1. sentinel monitor mymaster 127.0.0.1 6379 2

這行配置指示 Sentinel 去監視一個名為 mymaster 的主伺服器, 這個主伺服器的 IP 地址為 127.0.0.1 , 埠號為 6300, 而將這個主伺服器判斷為失效至少需要 2 個 Sentinel 同意,只要同意 Sentinel 的數量不達標,自動故障遷移就不會執行。

不過要注意, 無論你設定要多少個 Sentinel 同意才能判斷一個伺服器失效, 一個 Sentinel 都需要獲得系統中多數(majority) Sentinel 的支援, 才能發起一次自動故障遷移, 並預留一個給定的配置紀元 (configuration Epoch ,一個配置紀元就是一個新主伺服器配置的版本號)。換句話說, 在只有少數(minority) Sentinel 程序正常運作的情況下, Sentinel 是不能執行自動故障遷移的。sentinel叢集中各個sentinel也有互相通訊,通過gossip協議。

除了第一行配置,我們發現剩下的配置都有一個統一的格式:

  1. sentinel <option_name> <master_name> <option_value>

接下來我們根據上面格式中的option_name一個一個來解釋這些配置項:

  • down-after-milliseconds 選項指定了 Sentinel 認為伺服器已經斷線所需的毫秒數。
  • parallel-syncs 選項指定了在執行故障轉移時, 最多可以有多少個從伺服器同時對新的主伺服器進行同步, 這個數字越小, 完成故障轉移所需的時間就越長。

啟動 Sentinel

對於 redis-sentinel 程式, 你可以用以下命令來啟動 Sentinel 系統

  1. redis-sentinel sentinel.conf

對於 redis-server 程式, 你可以用以下命令來啟動一個執行在 Sentinel 模式下的 Redis 伺服器

  1. redis-server sentinel.conf --sentinel

以上兩種方式,都必須指定一個sentinel的配置檔案sentinel.conf, 如果不指定將無法啟動sentinel。sentinel預設監聽26379埠,所以執行前必須確定該埠沒有被別的程序佔用。

測試

此時,我們開啟兩個Sentinel,關閉主伺服器,我們來驗證下Sentinel。發現,伺服器發生切換了。

當6300埠的這個服務重啟的時候,他會變成6301埠服務的slave。

Twemproxy

Twemproxy是由Twitter開源的Redis代理, Redis客戶端把請求傳送到Twemproxy,Twemproxy根據路由規則傳送到正確的Redis例項,最後Twemproxy把結果彙集返回給客戶端。

Twemproxy通過引入一個代理層,將多個Redis例項進行統一管理,使Redis客戶端只需要在Twemproxy上進行操作,而不需要關心後面有多少個Redis例項,從而實現了Redis叢集。

Twemproxy本身也是單點,需要用Keepalived做高可用方案。

這麼些年來,Twenproxy作為應用範圍最廣、穩定性最高、最久經考驗的分散式中介軟體,在業界廣泛使用。

但是,Twemproxy存在諸多不方便之處,最主要的是,Twemproxy無法平滑地增加Redis例項,業務量突增,需增加Redis伺服器;業務量萎縮,需要減少Redis伺服器。但對Twemproxy而言,基本上都很難操作。其次,沒有友好的監控管理後臺介面,不利於運維監控。

Codis

Codis解決了Twemproxy的這兩大痛點,由豌豆莢於2014年11月開源,基於Go和C開發、現已廣泛用於豌豆莢的各種Redis業務場景。

Codis 3.x 由以下元件組成:

  • Codis Server:基於 redis-2.8.21 分支開發。增加了額外的資料結構,以支援 slot 有關的操作以及資料遷移指令。具體的修改可以參考文件 redis 的修改。
  • Codis Proxy:客戶端連線的 Redis 代理服務, 實現了 Redis 協議。 除部分命令不支援以外(不支援的命令列表),表現的和原生的 Redis 沒有區別(就像 Twemproxy)。對於同一個業務叢集而言,可以同時部署多個 codis-proxy 例項;不同 codis-proxy 之間由 codis-dashboard 保證狀態同步。
  • Codis Dashboard:叢集管理工具,支援 codis-proxy、codis-server 的新增、刪除,以及據遷移等操作。在叢集狀態發生改變時,codis-dashboard 維護叢集下所有 codis-proxy 的狀態的一致性。對於同一個業務叢集而言,同一個時刻 codis-dashboard 只能有 0個或者1個;所有對叢集的修改都必須通過 codis-dashboard 完成。
  • Codis Admin:叢集管理的命令列工具。可用於控制 codis-proxy、codis-dashboard 狀態以及訪問外部儲存。
  • Codis FE:叢集管理介面。多個叢集例項共享可以共享同一個前端展示頁面;通過配置檔案管理後端 codis-dashboard 列表,配置檔案可自動更新。
  • Codis HA:為叢集提供高可用。依賴 codis-dashboard 例項,自動抓取叢集各個元件的狀態;會根據當前叢集狀態自動生成主從切換策略,並在需要時通過 codis-dashboard 完成主從切換。
  • Storage:為叢集狀態提供外部儲存。提供 Namespace 概念,不同叢集的會按照不同 product name 進行組織;目前僅提供了 Zookeeper 和 Etcd 兩種實現,但是提供了抽象的 interface 可自行擴充套件。

Codis引入了Group的概念,每個Group包括1個Redis Master及一個或多個Redis Slave,這是和Twemproxy的區別之一,實現了Redis叢集的高可用。當1個Redis Master掛掉時,Codis不會自動把一個Slave提升為Master,這涉及資料的一致性問題,Redis本身的資料同步是採用主從非同步複製,當資料在Maste寫入成功時,Slave是否已讀入這個資料是沒法保證的,需要管理員在管理介面上手動把Slave提升為Master。

Codis使用,可以參考官方文件https://github.com/CodisLabs/codis/blob/release3.0/doc/tutorial_zh.md

Redis 3.0叢集

Redis 3.0叢集採用了P2P的模式,完全去中心化。支援多節點資料集自動分片,提供一定程度的分割槽可用性,部分節點掛掉或者無法連線其他節點後,服務可以正常執行。Redis 3.0叢集採用Hash Slot方案,而不是一致性雜湊。Redis把所有的Key分成了16384個slot,每個Redis例項負責其中一部分slot。叢集中的所有資訊(節點、埠、slot等),都通過節點之間定期的資料交換而更新。

Redis客戶端在任意一個Redis例項發出請求,如果所需資料不在該例項中,通過重定向命令引導客戶端訪問所需的例項。

Redis 3.0叢集,目前支援的cluster特性

  • 節點自動發現
  • slave->master 選舉,叢集容錯
  • Hot resharding:線上分片
  • 叢集管理:cluster xxx
  • 基於配置(nodes-port.conf)的叢集管理
  • ASK 轉向/MOVED 轉向機制

如上圖所示,所有的redis節點彼此互聯(PING-PONG機制),內部使用二進位制協議優化傳輸速度和頻寬。節點的fail是通過叢集中超過半數的節點檢測失效時才生效。客戶端與redis節點直連,不需要中間proxy層。客戶端不需要連線叢集所有節點,連線叢集中任何一個可用節點即可。redis-cluster把所有的物理節點對映到[0-16383]slot上cluster負責維護node<->slot<->value。


選舉過程是叢集中所有master參與,如果半數以上master節點與master節點通訊超時,認為當前master節點掛掉。

當叢集不可用時,所有對叢集的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)錯誤。如果叢集任意master掛掉,且當前master沒有slave,叢集進入fail狀態,也可以理解成進群的slot對映[0-16383]不完成時進入fail狀態。如果進群超過半數以上master掛掉,無論是否有slave叢集進入fail狀態。

環境搭建

現在,我們進行叢集環境搭建。叢集環境至少需要3個主伺服器節點。本次測試,使用另外3個節點作為從伺服器的節點,即3個主伺服器,3個從伺服器。

修改配置檔案,其它的保持預設即可。

  1. # 根據實際情況修改
  2. port 7000
  3. # 允許redis支援叢集模式
  4. cluster-enabled yes
  5. # 節點配置檔案,由redis自動維護
  6. cluster-config-file nodes.conf
  7. # 節點超時毫秒
  8. cluster-node-timeout 5000
  9. # 開啟AOF同步模式
  10. appendonly yes

建立叢集

目前這些例項雖然都開啟了cluster模式,但是彼此還不認識對方,接下來可以通過Redis叢集的命令列工具redis-trib.rb來完成叢集建立。
首先,下載 https://raw.githubusercontent.com/antirez/redis/unstable/src/redis-trib.rb

然後,搭建Redis 的 Ruby 支援環境。這裡,不進行擴充套件,參考相關文件。

現在,接下來執行以下命令。這個命令在這裡用於建立一個新的叢集, 選項–replicas 1 表示我們希望為叢集中的每個主節點建立一個從節點。

  1. redis-trib.rb create --replicas 1 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006

5.3、測試

七、Redis 為什麼是單執行緒的

Redis為什麼是單執行緒的?

因為CPU不是Redis的瓶頸。Redis的瓶頸最有可能是機器記憶體或者網路頻寬。(以上主要來自官方FAQ)既然單執行緒容易實現,而且CPU不會成為瓶頸,那就順理成章地採用單執行緒的方案了。關於redis的效能,官方網站也有,普通筆記本輕鬆處理每秒幾十萬的請求,參見:How fast is Redis?

 

如果萬一CPU成為你的Redis瓶頸了,或者,你就是不想讓伺服器其他核閒置,那怎麼辦?

那也很簡單,你多起幾個Redis程序就好了。Redis是keyvalue資料庫,又不是關係資料庫,資料之間沒有約束。只要客戶端分清哪些key放在哪個Redis程序上就可以了。redis-cluster可以幫你做的更好。

 

單執行緒可以處理高併發請求嗎?

當然可以了,Redis都實現了。

有一點概念需要澄清,併發並不是並行。

(相關概念:併發性I/O流,意味著能夠讓一個計算單元來處理來自多個客戶端的流請求。並行性,意味著伺服器能夠同時執行幾個事情,具有多個計算單元)

 

Redis總體快速的原因:

採用佇列模式將併發訪問變為序列訪問(?)

單執行緒指的是網路請求模組使用了一個執行緒(所以不需考慮併發安全性),其他模組仍用了多個執行緒。

總體來說快速的原因如下:

1)絕大部分請求是純粹的記憶體操作(非常快速)

2)採用單執行緒,避免了不必要的上下文切換和競爭條件

3)非阻塞IO

內部實現採用epoll,採用了epoll+自己實現的簡單的事件框架。epoll中的讀、寫、關閉、連線都轉化成了事件,然後利用epoll的多路複用特性,絕不在io上浪費一點時間

這3個條件不是相互獨立的,特別是第一條,如果請求都是耗時的,採用單執行緒吞吐量及效能可想而知了。應該說redis為特殊的場景選擇了合適的技術方案。

 

======

 

1. Redis服務端是個單執行緒的架構,不同的Client雖然看似可以同時保持連線,但發出去的命令是序列化執行的,這在通常的資料庫理論下是最高級別的隔離(serialize)
2. 用MULTI/EXEC 來把多個命令組裝成一次傳送,達到原子性
3. 用WATCH提供的樂觀鎖功能,在你EXEC的那一刻,如果被WATCH的鍵發生過改動,則MULTI到EXEC之間的指令全部不執行,不需要rollback
4. 其他回答中提到的DISCARD指令只是用來撤銷EXEC之前被暫存的指令,並不是回滾

八、快取雪崩

由於原有的快取過期失效,新的快取還沒有快取進來,有一隻請求快取請求不到,導致所有請求都跑去了資料庫,導致資料庫IO、記憶體和CPU眼裡過大,甚至導致宕機,使得整個系統崩潰。

解決思路:
1,採用加鎖計數,或者使用合理的佇列數量來避免快取失效時對資料庫造成太大的壓力。這種辦法雖然能緩解資料庫的壓力,但是同時又降低了系統的吞吐量。
2,分析使用者行為,儘量讓失效時間點均勻分佈。避免快取雪崩的出現。
3,如果是因為某臺快取伺服器宕機,可以考慮做主備,比如:redis主備,但是雙快取涉及到更新事務的問題,update可能讀到髒資料,需要好好解決。

加鎖:加鎖排隊只是為了減輕資料庫的壓力,並沒有提高系統吞吐量。假設在高併發下,快取重建期間key是鎖著的,這是過來1000個請求999個都在阻塞的。同樣會導致使用者等待超時,這是個治標不治本的方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

public class CacheDemo

{

    public object GetCacheDataList()

        {

            const int cacheTime = 60;   

            const string lockKey = cacheKey;

            const string cacheKey = "datainfolist";      

            var cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)

            {

                return cacheValue;

            }

            else

            {

                lock (lockKey)

                {

                    cacheValue = CacheHelper.Get(cacheKey);

                    if (cacheValue != null)

                    {

                        return cacheValue;

                    }

                    else

                    {

                        cacheValue = GetDataBaseInfo();              

                        CacheHelper.Add(cacheKey, cacheValue, cacheTime);

                    }                   

                }

                return cacheValue;

            }

        }

}

  標記失效快取:

快取標記:記錄快取資料是否過期,如果過期會觸發通知另外的執行緒在後臺去更新實際key的快取。

  快取資料:它的過期時間比快取標記的時間延長1倍,例:標記快取時間30分鐘,資料快取設定為60分鐘。 這樣,當快取標記key過期後,實際快取還能把舊資料返回給呼叫端,直到另外的執行緒在後臺更新完成後,才會返回新快取。

  這樣做後,就可以一定程度上提高系統吞吐量。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public object GetProductListNew()

        {

            const int cacheTime = 30;

            const string cacheKey = "product_list";

            //快取標記。

            const string cacheSign = cacheKey + "_sign";

             

            var sign = CacheHelper.Get(cacheSign);

            //獲取快取值

            var cacheValue = CacheHelper.Get(cacheKey);

            if (sign != null)

            {

                return cacheValue; //未過期,直接返回。

            }

            else

            {

                CacheHelper.Add(cacheSign, "1", cacheTime);

                ThreadPool.QueueUserWorkItem((arg) =>

                {

                    cacheValue = GetProductListFromDB(); //這裡一般是 sql查詢資料。

                    CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期設快取時間的2倍,用於髒讀。               

                });

                 

                return cacheValue;

            }

        }

九、快取穿透:

快取穿透是指使用者查詢資料,在資料庫沒有,自然在快取中也不會有。這樣就導致使用者查詢的時候,在快取中找不到,每次都要去資料庫再查詢一遍,然後返回空。這樣請求就繞過快取直接查資料庫,這也是經常提的快取命中率問題。

  解決的辦法就是:如果查詢資料庫也為空,直接設定一個預設值存放到快取,這樣第二次到緩衝中獲取就有值了,而不會繼續訪問資料庫,這種辦法最簡單粗暴。

快取穿透是指使用者查詢資料,在資料庫沒有,自然在快取中也不會有。這樣就導致使用者查詢的時候,在快取中找不到,每次都要去資料庫中查詢。

解決思路:

1,如果查詢資料庫也為空,直接設定一個預設值存放到快取,這樣第二次到緩衝中獲取就有值了,而不會繼續訪問資料庫,這種辦法最簡單粗暴。

2,根據快取資料Key的規則。例如我們公司是做機頂盒的,快取資料以Mac為Key,Mac是有規則,如果不符合規則就過濾掉,這樣可以過濾一部分查詢。在做快取規劃的時候,Key有一定規則的話,可以採取這種辦法。這種辦法只能緩解一部分的壓力,過濾和系統無關的查詢,但是無法根治。

3,採用布隆過濾器,將所有可能存在的資料雜湊到一個足夠大的BitSet中,不存在的資料將會被攔截掉,從而避免了對底層儲存系統的查詢壓力。關於布隆過濾器,詳情檢視:基於BitSet的布隆過濾器(Bloom Filter) 

大併發的快取穿透會導致快取雪崩。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public object GetProductListNew()

        {

            const int cacheTime = 30;

            const string cacheKey = "product_list";

 

            var cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)

                return cacheValue;

                 

            cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)

            {

                return cacheValue;

            }

            else

            {

                cacheValue = GetProductListFromDB(); //資料庫查詢不到,為空。

                 

                if (cacheValue == null)

                {

                    cacheValue = string.Empty; //如果發現為空,設定個預設值,也快取起來。               

                }

                CacheHelper.Add(cacheKey, cacheValue, cacheTime);

                 

                return cacheValue;

            }

        }

  把空結果,也給快取起來,這樣下次同樣的請求就可以直接返回空了,即可以避免當查詢的值為空時引起的快取穿透。同時也可以單獨設定個快取區域儲存空值,對要查詢的key進行預先校驗,然後再放行給後面的正常快取處理邏輯。

 

 

快取預熱

  快取預熱就是系統上線後,將相關的快取資料直接載入到快取系統。這樣避免,使用者請求的時候,再去載入相關的資料。

  解決思路:

    1,直接寫個快取重新整理頁面,上線時手工操作下。

    2,資料量不大,可以在WEB系統啟動的時候載入。

    3,定時重新整理快取,

 

快取更新

  快取淘汰的策略有兩種:

    (1) 定時去清理過期的快取。

    (2)當有使用者請求過來時,再判斷這個請求所用到的快取是否過期,過期的話就去底層系統得到新資料並更新快取。 

  兩者各有優劣,第一種的缺點是維護大量快取的key是比較麻煩的,第二種的缺點就是每次使用者請求過來都要判斷快取失效,邏輯相對比較複雜,具體用哪種方案,大家可以根據自己的應用場景來權衡。1. 預估失效時間 2. 版本號(必須單調遞增,時間戳是最好的選擇)3. 提供手動清理快取的介面。

 

 

 

十、使用快取的合理性問題

如何使用快取,怎麼才能更加合理?今天的話題,結合我之前的專案場景,討論下使用快取合理性問題。

 

熱點資料,快取才有價值

對於冷資料而言,大部分資料可能還沒有再次訪問到就已經被擠出記憶體,不僅佔用記憶體,而且價值不大。

對於熱點資料,比如我們的某IM產品,生日祝福模組,當天的壽星列表,快取以後可能讀取數十萬次。再舉個例子,某導航產品,我們將導航資訊,快取以後可能讀取數百萬次。

頻繁修改的資料,看情況考慮使用快取

資料更新前至少讀取兩次,快取才有意義。這個是最基本的策略,如果快取還沒有起作用就失效了,那就沒有太大價值了。

對於上面兩個例子,壽星列表、導航資訊都存在一個特點,就是資訊修改頻率不高,讀取通常非常高的場景。

那存不存在,修改頻率很高,但是又不得不考慮快取的場景呢?有!比如,這個讀取介面對資料庫的壓力很大,但是又是熱點資料,這個時候就需要考慮通過快取手段,減少資料庫的壓力,比如我們的某助手產品的,點贊數,收藏數,分享數等是非常典型的熱點資料,但是又不斷變化,此時就需要將資料同步儲存到Redis快取,減少資料庫壓力。

資料不一致性

一般會對快取設定失效時間,一旦超過失效時間,就要從資料庫重新載入,因此應用要容忍一定時間的資料不一致。還有一種是在資料更新時立即更新快取,不過這也會更多系統開銷和事務一致性問題。

快取更新機制

使用快取過程中,我們經常會遇到快取資料的不一致性和與髒讀現象,我們有什麼解決策略呢?

一般情況下,我們採取快取雙淘汰機制,在更新資料庫的時候淘汰快取。此外,設定超時時間,例如30分鐘。極限場景下,即使有髒資料入cache,這個髒資料也最多存在三十分鐘。

快取可用性

快取是提高資料讀取效能的,快取資料丟失和快取不可用不會影響應用程式的處理。因此,一般的操作手段是,如果Redis出現異常,我們手動捕獲這個異常,記錄日誌,並且去資料庫查詢資料返回給使用者。

快取服務降級

服務降級的目的,是為了防止Redis服務故障,導致資料庫跟著一起發生雪崩問題。因此,對於不重要的快取資料,可以採取服務降級策略,例如一個比較常見的做法就是,Redis出現問題,不去資料庫查詢,而是直接返回預設值給使用者。

快取預熱

在新啟動的快取系統中,如果沒有任何資料,在重建快取資料過程中,系統的效能和資料庫複製都不太好,那麼最好的快取系統啟動時就把熱點資料載入好,例如對於快取資訊,在啟動快取載入資料庫中全部資料進行預熱。一般情況下,我們會開通一個同步資料的介面,進行快取預熱。

快取穿透

如果因為不恰當的業務,或者惡意攻擊持續地發請求某些不存在的資料,由於快取沒有儲存該資料,所有的請求都會落到資料庫上,會對資料庫造成很大壓力,甚至奔潰。一個簡單的對策是將不存在的資料也快取起來。