Redis雜談
Redis是近年來發展迅速的內存數據庫,網上也已經有多Redis的文章。但不管是英文還是中文,多數文章的各個知識點都比較分散,本系列是關于Redis主題的綜合性討論,也算是對我使用Redis的一個總結,主要面向已經使用Redis,但對于整體還不甚了解的Java程序員,當然也可以作為入門參考,對于重要的內容,本文力求把基本思想講到,限于篇幅不能深入的內容,會給出相關細節的參考來源,本文寫作時,Redis穩定版為 3.2.8
。本系列由三部分構成:
- (一)Redis雜談,主要包括Redis本身的性質,包括于Redis的介紹,基本操作,數據持久化,Reids集群相關內容。
- (二)Spring下使用Redis,主要包括Spring Data Redis
1.8.3
提供的對于Redis的各種操作和說明。 - (三)實現SimHash網頁去重,主要通個一個具體的例子,結合前述內容展示Redis的使用。
好了,Let's go!
Redis介紹
Redis是什么
先看看維基百科是怎么說的:
Redis是一個使用ANSI C編寫的開源、支持網絡、基于內存、可選持久性的鍵值對存儲數據庫。從2015年6月開始,Redis的開發由Redis Labs贊助,而2013年5月至2015年6月期間,其開發由Pivotal贊助。在2013年5月之前,其開發由VMware贊助。根據月度排行網站DB-Engines.com的數據顯示,Redis是最流行的鍵值對存儲數據庫 。
這個描述中,有幾個關鍵詞:開源
支持網絡
基于內存
可選持久性
鍵值對數據庫
,基本上概括了Redis的核心特征。Redis通過TCP套接字和一個簡單的協議構建了一個服務器-客戶端模式,因此,不同的進程能夠以一種共享的方式查詢和修改數據。
具體來說:
- 基于內存:是說,我運行的數據都加載到內存里面,潛臺詞就是,我很快哦,很快很快哦!
- NoSQL:對不起,我沒有SQL解析引擎,我沒法這樣玩:select name from employee where age < 60 。
- 可選持久性:斷電了或者機器崩潰了,我可以恢復,而且持久化策略可以配置。
- 鍵值對:所有數據都通過Key來存取,復雜度O(1)。
- 集群:可以組團群毆,而且群毆方案豐富多樣。
Redis的特點
理解Redis的特點,最好的入口,就是理解Redis常被形容為數據結構服務器,這到底是個啥?這的確是一個不常見的術語,所以Redis在首頁就掛了這么一段話來解釋自己對自己的定位:
Redis支持諸如strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs 和 geospatial indexes with radius等形式的數據結構,它內建了復制,Lua腳本,LRU緩存機制,事務,不同級別的數據持久化,并通過Redis Sentinel提供高可用性,和通過Redis Cluster提供自動分區。
其他類似產品
下圖是DB-Engines.com根據一些特征,對使用Key-Value存儲模式的數據庫引擎進行的排名。常常拿來和Redis比較的,可能就是排在第二位的Memcached了。
雖然Memcached和Redis有眾多不同,比如線程模式、存儲模式等,但如果一言蔽之,誠如這篇StackOverflow中提到的:
Memcached 是一個非持久化的內存key/value數據庫,Redis也能做得這一點, 但Redis還是一個可持久化的數據結構服務器。
Redis的各種資料獲取
Redis的資料非常豐富,建議優先閱讀和查詢官方的文檔:
- Redis的數據類型介紹: http://redis.io/topics/data-types-intro
- 所有Redis命令列表: http://redis.io/commands
- 更多的Redis官方文檔: http://redis.io/documentation
- Spring Data Redis官方文檔:http://docs.spring.io/spring-data/redis/docs/current/reference/html
- Redis的漢化鏡像站: http://www.redis.cn/
- Salvatore(Redis發起者)的博客:http://oldblog.antirez.com/
Redis基本命令
Redis使用的是server-client模式,使用Redis存取數據,就是通過Redis客戶端向服務器端發送各種操作命令存取數據,大致的過程就是這樣:
redis> ping
PONG
redis> info
# Server
redis_version:3.2.8
....
這里有一份不錯的命令手冊中文翻譯。
需要強調和不容易理解的一點是: Redis的整體結構是Key-Value的,但是和其他一些Key-Value產品不一樣的是,這個Value本身可以是有數據結構的,比如Value本身可以是這么多類型:
- Strings
- Hashes
- Lists
- Sets
- Sorted sets with range queries
- Hyperloglogs
- Geospatial indexes with radius queries.
怎么理解這一點呢,我們通過一個例子來說明:
redis> HSET myhash field1 "foo"
(integer) 1
redis> HGET myhash field1
"foo"
redis> HGET myhash field2
(nil)
redis> HSET myhash field2 "bar"
(integer) 1
redis> HMGET myhash field1 field2
1) "foo"
2) "bar"
這個例子存儲的類型是Hash,也就是說Value本身也是一個Key-Value結構的數據。整體上就是Key-(Key-Value),我們可以理解為myhash是一個Hash表的名字,filed1,filed2是myhash這張Hash表中的鍵值,而myhash同時也是Redis這張大Hash表中的一個Key。
同理,不管Value是什么類型,它都有一個Key,這個Key就是Redis本身Key-Vaule的Key。
好的,現在就可以去對照命令手冊使用Redis,什么,你還沒有Redis的環境!沒關系,Redis官方提供了一個 ==網頁版Redis體驗== 的供大家練手:
Redis的安裝與配置
安裝
單機版的Redis的安裝非常簡單,Redis兼容的操作系統為:Linux, OSX, OpenBSD, NetBSD, FreeBSD。支持Big endian和Little endian 處理器架構, 支持64位和32位系統。
在Linux下的安裝過程,只需要make命令就可:
% make
如果是32位系統:
% make 32bit
編譯后可以通過make test測試:
% make test
如有問題,可參考官方編譯說明。
Redis官方沒有Windows版本,但是微軟實現了一個Windows版的Redis Server 。
配置
Redis的配置文件是自注釋的,寫的密密麻麻,含義非常詳細,清楚。運行時,把配置路徑作為參數啟動,使配置生效:
redis>redis-server /opt/redis/redis.conf
總共的配置項目超過50項,不過,在非集群模式下,通常關注的配置項目只有這些:
- maxmemory [3000m] 最大使用內存
- daemonize [yes|no] 是否后臺啟動
- dir [path] 持久化數據存放目錄
- requirepass [password] 登錄密碼
- save [seconds] [changes] 在多少秒內有多少次寫操作,就刷入一次數據到磁盤
- appendonly [yes|no] 是否開啟APPEND ONLY模式,這也是一種持久化策略,下一節會介紹
這幾個常用配置都非常好理解,但是第一個maxmemory要需要注意,這個配置涉及到配置Eviction policies,更多內容可以參考 LRU算法進行緩存回收。
也可以參考這一份不錯的中文配置說明,但是由于Redis發展非常迅速,所以生產環境中使用的配置項一定要對照官方說明。
比如Redis配置中的VM配置(虛擬內存機制),很多文章還在提,但是這個配置其實已經在不斷的發展中被廢棄了,這個配置的用意是VM機制將數據分頁存放,由Redis將訪問量較少的頁即冷數據swap到磁盤上,訪問多的頁面由磁盤自動換出到內存中,棒棒噠,對吧?但是對于Redis這么一個小軟件,希望把存儲做成如同Oracle一樣的方式,具備自動淘汰冷熱數據功能,并且比Linux操作系統本身更加優秀,太難了。
對于Linux系統,在配置文件之外,還有一些配置需要考慮:
修改內存分配策略,使系統請求分配內存時,永遠假裝還有足夠的內存
echo 'vm.overcommit_memory = 1' >>/etc/sysctl.conf
然后執行: sysctl vm.overcommit_memory=1
定義了系統中每一個端口最大的監聽隊列的長度,這是個全局的參數,默認128。
echo 1024 > /proc/sys/net/core/somaxconn
禁用透明緩存頁
echo never > /sys/kernel/mm/transparent_hugepage/enabled
關于這部分的內容,可以參考 Redis Administration 和 Redis latency problems troubleshooting。
Redis 的數據持久化
理解Redis的數據持久化對于使用Redis特別重要,因為? 當然是因為不能隨便把數據搞丟,還有什么比這更重要么!況且可配置持久化,也是很多用戶選擇Redis的重要原因。
Redis官方有一篇專門闡述其持久化的文檔,以及Redis開發者Salvatore針對這個問題撰寫的一篇長文《Redis 持久化解密》。
不管他們怎么說,其實歸納起來我們就想知道3個問題:
- Redis的持久化是如何工作的?
- 這樣工作對性能的影響有多大?
- 我應該如何選擇?
一般來說Redis的所有工作數據都在內存中,這也是內存數據庫的特點,持久化數據只是啟動時加載,或者作為災備手段。前面已經提到了Redis的持久化有兩種方式:RDB和AOF。
RDB持久化方式能夠在指定的時間間隔能對你的數據進行快照存儲,它是一個非常緊湊的文件,它保存了某個時間點得數據集,非常適用于數據集的備份,比如你可以在每個小時報保存一下過去24小時內的數據,同時每天保存過去30天的數據,這樣即使出了問題你也可以根據需求恢復到不同版本的數據集。
RDB的持久化方式被稱為快照,在默認情況下,Redis將數據庫快照保存在名字為dump.rdb的二進制文件中。你可以對 Redis 進行設置, 讓它在“ N 秒內數據集至少有 M 個改動”這一條件被滿足時, 自動保存一次數據集。你也可以通過調用 SAVE或者 BGSAVE , 手動讓 Redis 進行數據集保存操作。
比如說, 以下設置會讓 Redis 在滿足“ 60 秒內有至少有 1000 個鍵被改動”這一條件時, 自動保存一次數據集:
save 60 1000
AOF持久化方式記錄每次對服務器寫的操作,當服務器重啟的時候會重新執行這些命令來恢復原始的數據,AOF命令以redis協議追加保存每次寫的操作到文件末尾。Redis還能對AOF文件進行后臺重寫,使得AOF文件的體積不至于過大。一個AOF文件就像這個樣子:
$ cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$4
key1
$5
Hello
*3
$6
append
$4
key1
$7
World!
*2
$3
del
$4
key1
你可以配置 Redis 多久才將數據 fsync 到磁盤一次。有三種方式:
- 每次有新命令追加到 AOF 文件時就執行一次 fsync :非常慢,也非常安全
- 每秒 fsync 一次:足夠快(和使用 RDB 持久化差不多),并且在故障時只會丟失 1 秒鐘的數據。
- 從不 fsync :將數據交給操作系統來處理。更快,也更不安全的選擇。
官方推薦(并且也是默認)的措施為每秒 fsync 一次, 這種 fsync 策略可以兼顧速度和安全性。
有兩點需要注意:
如果同時開啟兩種持久化方式,在這種情況下, 當redis重啟的時候會優先載入AOF文件來恢復原始的數據,因為在通常情況下AOF文件保存的數據集要比RDB文件保存的數據集要完整。
RDB快照會被用于master -> slave同步。
優缺點
RDB的優點
RDB是一個非常緊湊的文件,它保存了某個時間點得數據集,非常適用于數據集的備份,比如你可以在每個小時報保存一下過去24小時內的數據,同時每天保存過去30天的數據,這樣即使出了問題你也可以根據需求恢復到不同版本的數據集。
RDB是一個緊湊的單一文件,很方便傳送到另一個遠端數據中心或者亞馬遜的S3(可能加密),非常適用于災難恢復。
RDB在保存RDB文件時父進程唯一需要做的就是fork出一個子進程,接下來的工作全部由子進程來做,父進程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。
與AOF相比,在恢復大的數據集的時候,RDB方式會更快一些。
RDB的缺點
如果你希望在redis意外停止工作(例如電源中斷)的情況下丟失的數據最少的話,那么RDB不適合你。雖然你可以配置不同的save時間點(例如每隔5分鐘并且對數據集有100個寫的操作,是Redis要完整的保存整個數據集是一個比較繁重的工作,你通常會每隔5分鐘或者更久做一次完整的保存,萬一在Redis意外宕機,你可能會丟失幾分鐘的數據。
RDB 需要經常fork子進程來保存數據集到硬盤上,當數據集比較大的時候,fork的過程是非常耗時的,可能會導致Redis在一些毫秒級內不能響應客戶端的請求。如果數據集巨大并且CPU性能不是很好的情況下,這種情況會持續1秒,AOF也需要fork,但是你可以調節重寫日志文件的頻率來提高數據集的耐久度。
AOF 優點
使用AOF 會讓你的Redis更加耐久: 你可以使用不同的fsync策略:無fsync,每秒fsync,每次寫的時候fsync。使用默認的每秒fsync策略,Redis的性能依然很好(fsync是由后臺線程進行處理的,主線程會盡力處理客戶端請求),一旦出現故障,你最多丟失1秒的數據。
AOF文件是一個只進行追加的日志文件,所以不需要寫入seek,即使由于某些原因(磁盤空間已滿,寫的過程中宕機等等)未執行完整的寫入命令,你也也可使用redis-check-aof工具修復這些問題。
Redis 可以在 AOF 文件體積變得過大時,自動地在后臺對 AOF 進行重寫: 重寫后的新 AOF 文件包含了恢復當前數據集所需的最小命令集合。 整個重寫操作是絕對安全的,因為 Redis 在創建新 AOF 文件的過程中,會繼續將命令追加到現有的 AOF 文件里面,即使重寫過程中發生停機,現有的 AOF 文件也不會丟失。 而一旦新 AOF 文件創建完畢,Redis 就會從舊 AOF 文件切換到新 AOF 文件,并開始對新 AOF 文件進行追加操作。
AOF 文件有序地保存了對數據庫執行的所有寫入操作, 這些寫入操作以 Redis 協議的格式保存, 因此 AOF 文件的內容非常容易被人讀懂, 對文件進行分析(parse)也很輕松。 導出(export) AOF 文件也非常簡單: 舉個例子, 如果你不小心執行了 FLUSHALL 命令, 但只要 AOF 文件未被重寫, 那么只要停止服務器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重啟 Redis , 就可以將數據集恢復到 FLUSHALL 執行之前的狀態。
AOF 缺點
對于相同的數據集來說,AOF 文件的體積通常要大于 RDB 文件的體積。
根據所使用的 fsync 策略,AOF 的速度可能會慢于 RDB 。 在一般情況下, 每秒 fsync 的性能依然非常高, 而關閉 fsync 可以讓 AOF 的速度和 RDB 一樣快, 即使在高負荷之下也是如此。 不過在處理巨大的寫入載入時,RDB 可以提供更有保證的最大延遲時間(latency)。
如何選擇
關于如何選擇,官方這么說:
一般來說, 如果想達到足以媲美 PostgreSQL 的數據安全性, 你應該同時使用兩種持久化功能。
如果你非常關心你的數據, 但仍然可以承受數分鐘以內的數據丟失, 那么你可以只使用 RDB 持久化。
有很多用戶都只使用 AOF 持久化, 但我們并不推薦這種方式: 因為定時生成 RDB 快照(snapshot)非常便于進行數據庫備份, 并且 RDB 恢復數據集的速度也要比 AOF 恢復的速度要快, 除此之外, 使用 RDB 還可以避免之前提到的 AOF 程序的 bug 。
注意: 因為以上提到的種種原因, 未來我們可能會將 AOF 和 RDB 整合成單個持久化模型。
Two More Things ^_^
首先,官方所謂“未來我們可能會將 AOF 和 RDB 整合成單個持久化模型”在某種程度上已經在4.0版本中實現了,這個改進可以分為兩個層次來看:其一是AOF的實現機制,導致AOF文件太大,4.0 可以配置AOF,使其僅僅進行增量記錄;其二是集群下主備必須全量復制,這種機制被更改為稱為PSYNC2.0的帶標簽復制,看到Salvatore給五年前的一個留言的回復,我想他那刻的內心必是喜悅的(被你們TM懟了5年了啦)。
其次,我們回過頭來,討論一下到底什么叫數據持久化。非常簡化的來看,數據持久化可以分為這么5步:
- 客戶端發送一個寫命令到數據庫(數據在客戶端的內存中)。
- 數據庫接收到這個寫命令(數據在服務器的內存中)。
- 數據庫調用系統調用把寫數據存入磁盤(數據在內核緩沖區kernel's buffer)
- 操作系統把寫緩沖區數據傳輸到磁盤控制器(數據在磁盤緩存中)
- 磁盤控制器真正把數據寫到物理介質上。
傳統的Unix系統(Linux)實現在內核中沒有緩沖區高速緩存或頁高速緩存,大多數磁盤I/O都通過緩沖區進行。當我們向文件寫入數據時(比如 POSIX API 的write系統調用),內核通常先將數據復制到緩沖區中,然后排入隊列,晚些時候再寫入磁盤。這種方式被稱為延遲寫。通常,當內核需要重用緩沖區來存放其他磁盤數據時,他會把所有延遲寫數據寫入磁盤。為保證磁盤上實際文件與緩沖區中的內容一致,Unix系統提供了sync,fsync和fdatasync三個函數。
其中,sync與fsync的區別在于,sync只是將所有修改過的塊緩沖區排入寫隊列,然后返回,它并不等待實際寫磁盤操作結束,一般update系統守護進程會周期性調用sync函數(Linux是30s),fsync函數需要傳入文件描述符,它會等到磁盤寫操作結束才返回。可以想見,雖然每個操作都調用fsync是最保險的做法,但是這種大量隨機尋址對于任何運行于Rotational disks的應用來說,都是非常慢的。
上述內容旨在讓讀者理解數據持久化為什么需要各種策略,以及各種策略的意義,都是點到為止。
Redis集群
這一節,我們會談論Redis的集群,。集群簡單的理解就是一堆機器齊心協力提供某種類型的服務。對于Redis集群,我們關心這么幾個內容:數據是如何分布的,消息是如何傳遞的,出現異常的如何應對。Redis集群研究和實踐(基于redis 3.0.5)這篇文章根據官方的安裝指南,記錄了非常詳細的安裝和操作步驟,本文就不在贅述了,我們主要重點理解一下前面提到的三個問題。
一致性哈希
之所以要介紹這個概念,是因為一致性哈希是Web Cache類系統,用于數據分布最典型的設計(Memcached使用一致性哈希),我們將在后面一節與Redis的哈希槽(hash slot)進行一個比較。
一致性哈希在Wiki上講的非常清楚:
需求
在使用n臺緩存服務器時,一種常用的負載均衡方式是,對資源o的請求使用hash(o) = o mod n來映射到某一臺緩存服務器。當增加或減少一臺緩存服務器時這種方式可能會改變所有資源對應的hash值,也就是所有的緩存都失效了,這會使得緩存服務器大量集中地向原始內容服務器更新緩存。因些需要一致哈希算法來避免這樣的問題。
一致哈希盡可能使同一個資源映射到同一臺緩存服務器。這種方式要求增加一臺緩存服務器時,新的服務器盡量分擔存儲其他所有服務器的緩存資源。減少一臺緩存服務器時,其他所有服務器也可以盡量分擔存儲它的緩存資源。 一致哈希算法的主要思想是將每個緩存服務器與一個或多個哈希值域區間關聯起來,其中區間邊界通過計算緩存服務器對應的哈希值來決定。(定義區間的哈希函數不一定和計算緩存服務器哈希值的函數相同,但是兩個函數的返回值的范圍需要匹配。)如果一個緩存服務器被移除,則它會從對應的區間會被并入到鄰近的區間,其他的緩存服務器不需要任何改變。
也許上個圖,更容易理解:
實現
一致哈希將每個對象映射到圓環邊上的一個點,系統再將可用的節點機器映射到圓環的不同位置。查找某個對象對應的機器時,需要用一致哈希算法計算得到對象對應圓環邊上位置,沿著圓環邊上查找直到遇到某個節點機器,這臺機器即為對象應該保存的位置。
當刪除一臺節點機器時,這臺機器上保存的所有對象都要移動到下一臺機器。添加一臺機器到圓環邊上某個點時,這個點的下一臺機器需要將這個節點前對應的對象移動到新機器上。 更改對象在節點機器上的分布可以通過調整節點機器的位置來實現。
其實,也要不了幾行代碼:
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHash<T> {
private final HashFunction hashFunction;
private final int numberOfReplicas;
private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
Collection<T> nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
add(node);
}
}
public void add(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.put(hashFunction.hash(node.toString() + i), node);
}
}
public void remove(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.remove(hashFunction.hash(node.toString() + i));
}
}
public T get(Object key) {
if (circle.isEmpty()) {
return null;
}
int hash = hashFunction.hash(key);
if (!circle.containsKey(hash)) {
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
}
Redis哈希槽與數據分布
Redis采用的是哈希槽的機制,它通過函數hash(key) = CRC16(key)%16384
將任意一個key映射到0-16383這個范圍,每個節點承接16384個key中的一段。如果增加或刪除一個節點,手動變更節點的承接范圍,具體的操作可以參考前面提到的《Redis集群研究和實踐》。
現在,我們再回到上面實現的這個一致性哈希,假設傳入了這樣一個hash函數,并且手動對node添加后的節點承載范圍進行調整:
private static final int RING = 16384;
public Integer hash(String key){
return CRC16(key)%RING;
}
public T get(Object key) {
return key-nodesTable.get(hash);
}
public void add(T node) {
//手動
//Step 1: ...
//Step 2: ...
}
這幾乎就是Reids的哈希槽方案。這樣的方案,簡單、粗暴、直接并且有效,不過就是要麻煩你動動手去規劃。我認為,一致性哈希的本質就是通過在Hash->Node之間虛擬一個中間層使之變成Hash->RING Point->Node,從而避免Node增刪帶來的全局映射變動。從這個意義上說,Redis的哈希槽就是一個簡化版的一致性哈希方案。
這里我們不評價這種簡化版一致性哈希方案的優劣,但它的確規避了系統對于節點增加或者刪除后,自動處理數據遷移,以及節點規劃給系統帶來的復雜性。
Gossip協議
Redis節點間的消息使用Gossip協議傳播,它常用于P2P的通信協議,這個協議就是模擬人類中傳播謠言的行為而來。
協議的核心內容就是節點通過將信息隨機發送到N個節點來完成本次信息的傳播,其涉及到周期性、配對、交互模式。Gossip的交互模式分為兩種:Anti-entropy和Rumor mongering。
- Anti-entropy:每個節點周期性地隨機選擇其他節點,然后通過相互交換自己的所有數據來消除兩者之間的差異。
- Rumor mongering:當一個節點有來新信息后,該節點變成活躍狀態,并周期性地聯系其他節點向其發送新信息。
每個節點維護一個自己的信息表<key, (value, version)>
,即屬性的值以及版本號;和一個記錄其他節點的信息表<node, <key, (value, version)>>
。每個節點和系統中的某個節點相互配對成為peer。而節點的信息交換方式主要有3種。
- Push:擁有狀態新信息的節點隨機選擇聯系節點并想起發送自己得到信息。
- Pull:發起信息交換的節點隨機選擇聯系節點并從對方獲取信息。
- Push-Pull混合模式:發起信息交換的節點向選擇的節點發送信息。
可以證明Gossip協議的傳播次數是收斂的。
傳播起來整個Redis集群內部一共有N*(N-1)條傳輸路徑,路徑真的實在太多了,以至于開發者畫出來的圖都少了兩條(紅線補齊),就大概就像這個樣子:
和Server與Client的不一樣,Redis內部節點間采用的是二進制協議以優化帶寬。Redis節點間的“謠言”,大概是這個樣子的:
部分故障
對于一個分布式系統,最大的挑戰就是要是節點掛了怎么辦,或者更具體的說如何知道一個節點是不是真的掛了,這也就是所謂的分布式系統的本質困難:“partial failure(部分故障)”。但是不得不說,Redis的實現弱化了這個困難,因為它沒有提供通常意義上說的高可用性。
當Redis集群中的一個主節點掛了之后,Goosip協議會選擇一個備節點替換上來,如果沒有備節點,整個集群系統就不可用了。是的,整體不可用!
這樣的設計避免了數據遷移和數據分布自動平衡,也避免了部分可用性需要進行的一些屏蔽和邏輯阻斷。
具體來說,Redis的每個節點都擁有一個與其他節點相關的狀態標示。有兩種狀態是用于失敗(失效)檢測的:PFAIL
標示和FAIL
標示。 PFAIL
意味著可能失敗,這一個還沒有得到確認的失敗類型。FAIL
意味著一個節點失敗已經在一個固定的時間范圍內被大多數主節點確認。
PFAIL
被確認為FAIL
需要滿足下面這些條件:
- A節點已經將B節點標示為
PFAIL
。 - 節點A通過gossip收集了集群中大多數主節點關于B的狀態記錄。
- 這些大多數的節點已經在
NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT
這個時間范圍內將B標記為PFAIL
或者FAIL
。
如果上述條件為真,那么節點A將做如下兩個動作:
- 標記B節點為
FAIL
。 - 把這個
FAIL
消息發送給其它所有可達的節點。
當然關于Redis的失敗檢測,還有更細節的內容和更復雜的情況,上面沒有提到,感興趣的讀者可以閱讀Redis集群規范。需要注意的是,FAIL
標識只是備節點提升為主節點的一個啟動條件。
節點選舉
備節點選舉和提升是備節點來處理的,并且需要主節點進行選舉。一個備節點選舉發生在一個主節點被它的至少一個備節點標記為FAIL
狀態,并且這些備節點具備成為主節點的先決條件下。
一個備節點為了把自己提升為主節點,它需要發起一輪選舉并且獲勝。一個主節點的所有備節點都可以在這個主節點處于FAIL
狀態下發起選舉,然而最后只有一個備節點能夠贏得選舉并提升自己成為主節點。
一個備節點發起一輪選舉必須滿足下面這些條件:
- 它的主節點處于
FAIL
狀態。 - 這個主節點承載了非零數量的哈希槽。
- 備節點與主節點的失聯時間在一個范圍內,這是為了確保備節點的數據足夠近,這個時間用戶可配置。
為了被選中,對于一個備節點來說,第一步就是增加自己的 currentEpoch
計數,并且從主節點實例請求選票。
備節點通過廣播一個FAILOVER_AUTH_REQUEST
包給每個主節點來請求選票。然后,它等待一個最大 NODE_TIMEOUT*2
(至少2秒)的時間接受回復。
一旦一個主節點投票給一個備節點,它主動回復一個FAILOVER_AUTH_ACK
,它不能NODE_TIMEOUT * 2
時間范圍內再給這個備節點的競爭對手投票。這不是必須的安全性保障,但是對于阻止多個備節點同時選上非常有用。
一個備節點會丟棄發送選舉請求后,小于當前 currentEpoch
周期的所有AUTH_ACK
回復。這確保了避免它錯誤地把上一輪選舉記票記到當前周期。
一旦一個備節點得到大多數主節點的ACKs,它就贏得了選舉。另外,如果這個大多數主節點在NODE_TIMEOUT*2
(至少2秒)時間內沒有達到,當前選舉會被廢棄,并且在NODE_TIMEOUT * 4
(至少4秒)時間后,嘗試開始一輪新的選舉。
Redis集群方案對比
關于不同的集群方案對比,阿里云有一篇軟文做了一些介紹,我認為:隨著Redis3.2.8的發布,Redis的集群已經基本可以應用于生產環境了。
關于不同集群對于高級功能的支持,軟文中有一個列表:
redis 4.0 | 阿里云redis | codis | |
---|---|---|---|
事務 | 支持相同slot | 支持相同的slot | 不支持 |
sub/pub | 支持相同slot | 支持 | 不支持 |
flushall | 支持 | 支持 | 不支持 |
select | 不支持 | 不支持 | 不支持 |
mset/mget | 支持相同slot | 支持 | 支持 |
以及性能對比:
這篇軟文中說:
在實際生產環境中,使用原生的redis cluster,客戶端需要實現cluster protocol, 解析move, ask等指令并重定向節點,隨意訪問key可能需要兩次訪問操作才能完成,性能上并不能完全如單節點一樣。
實際對于java來說,Jedis是支持redis cluster的,在后面一個主題“Spring下使用Redis”,我們會發現除非節點出現變動,幾乎所有的客戶端命令都可以一次完成,所以可以認為redis-cluster的性能就是實際應用時的性能,真是1core頂人家8core啊!
結語
關于Redis本身的內容我們就聊到這里,希望這篇文章能給大家起一個拋磚引玉的作用。鑒于作者水平有限,如果大家覺得什么地方不對,歡迎提出來,大家一起學習,一起進步。
最后附上Books在《人月神話》中的一句話,這句話來自于書中“貴族專制、民主政治和系統設計 ( Aristocracy,
Democracy, and System Design)”一節,是Redis作者Salvatore Sanfilippo的Google Group簽名,希望對你從一個側面理解Redis設計者的設計意圖:
If a system is to have conceptual integrity, someone must control the concepts.(如果要得到系統概念上的完整性, 那么必須有人控制這些概念)——— 《人月神話》