Redis雜談

Redis雜談

Redis是近年來發(fā)展迅速的內(nèi)存數(shù)據(jù)庫,網(wǎng)上也已經(jīng)有多Redis的文章。但不管是英文還是中文,多數(shù)文章的各個知識點都比較分散,本系列是關(guān)于Redis主題的綜合性討論,也算是對我使用Redis的一個總結(jié),主要面向已經(jīng)使用Redis,但對于整體還不甚了解的Java程序員,當(dāng)然也可以作為入門參考,對于重要的內(nèi)容,本文力求把基本思想講到,限于篇幅不能深入的內(nèi)容,會給出相關(guān)細(xì)節(jié)的參考來源,本文寫作時,Redis穩(wěn)定版為 3.2.8。本系列由三部分構(gòu)成:

  • (一)Redis雜談,主要包括Redis本身的性質(zhì),包括于Redis的介紹,基本操作,數(shù)據(jù)持久化,Reids集群相關(guān)內(nèi)容。
  • (二)Spring下使用Redis,主要包括Spring Data Redis 1.8.3 提供的對于Redis的各種操作和說明。
  • (三)實現(xiàn)SimHash網(wǎng)頁去重,主要通個一個具體的例子,結(jié)合前述內(nèi)容展示Redis的使用。

好了,Let's go!

Redis介紹

Redis是什么

先看看維基百科是怎么說的:

Redis是一個使用ANSI C編寫的開源、支持網(wǎng)絡(luò)、基于內(nèi)存、可選持久性的鍵值對存儲數(shù)據(jù)庫。從2015年6月開始,Redis的開發(fā)由Redis Labs贊助,而2013年5月至2015年6月期間,其開發(fā)由Pivotal贊助。在2013年5月之前,其開發(fā)由VMware贊助。根據(jù)月度排行網(wǎng)站DB-Engines.com的數(shù)據(jù)顯示,Redis是最流行的鍵值對存儲數(shù)據(jù)庫 。

這個描述中,有幾個關(guān)鍵詞:開源 支持網(wǎng)絡(luò) 基于內(nèi)存 可選持久性 鍵值對數(shù)據(jù)庫,基本上概括了Redis的核心特征。Redis通過TCP套接字和一個簡單的協(xié)議構(gòu)建了一個服務(wù)器-客戶端模式,因此,不同的進(jìn)程能夠以一種共享的方式查詢和修改數(shù)據(jù)。

具體來說:

  • 基于內(nèi)存:是說,我運行的數(shù)據(jù)都加載到內(nèi)存里面,潛臺詞就是,我很快哦,很快很快哦!
  • NoSQL:對不起,我沒有SQL解析引擎,我沒法這樣玩:select name from employee where age < 60 。
  • 可選持久性:斷電了或者機(jī)器崩潰了,我可以恢復(fù),而且持久化策略可以配置。
  • 鍵值對:所有數(shù)據(jù)都通過Key來存取,復(fù)雜度O(1)。
  • 集群:可以組團(tuán)群毆,而且群毆方案豐富多樣。

Redis的特點

理解Redis的特點,最好的入口,就是理解Redis常被形容為數(shù)據(jù)結(jié)構(gòu)服務(wù)器,這到底是個啥?這的確是一個不常見的術(shù)語,所以Redis在首頁就掛了這么一段話來解釋自己對自己的定位:

Redis支持諸如strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs 和 geospatial indexes with radius等形式的數(shù)據(jù)結(jié)構(gòu),它內(nèi)建了復(fù)制,Lua腳本,LRU緩存機(jī)制,事務(wù),不同級別的數(shù)據(jù)持久化,并通過Redis Sentinel提供高可用性,和通過Redis Cluster提供自動分區(qū)。

其他類似產(chǎn)品

下圖是DB-Engines.com根據(jù)一些特征,對使用Key-Value存儲模式的數(shù)據(jù)庫引擎進(jìn)行的排名。常常拿來和Redis比較的,可能就是排在第二位的Memcached了。

image

雖然Memcached和Redis有眾多不同,比如線程模式、存儲模式等,但如果一言蔽之,誠如這篇StackOverflow中提到的:

Memcached 是一個非持久化的內(nèi)存key/value數(shù)據(jù)庫,Redis也能做得這一點, 但Redis還是一個可持久化的數(shù)據(jù)結(jié)構(gòu)服務(wù)器。

Redis的各種資料獲取

Redis的資料非常豐富,建議優(yōu)先閱讀和查詢官方的文檔:

Redis基本命令

Redis使用的是server-client模式,使用Redis存取數(shù)據(jù),就是通過Redis客戶端向服務(wù)器端發(fā)送各種操作命令存取數(shù)據(jù),大致的過程就是這樣:

redis> ping
PONG
redis> info
# Server
redis_version:3.2.8
....

這里有一份不錯的命令手冊中文翻譯。

需要強(qiáng)調(diào)和不容易理解的一點是: Redis的整體結(jié)構(gòu)是Key-Value的,但是和其他一些Key-Value產(chǎn)品不一樣的是,這個Value本身可以是有數(shù)據(jù)結(jié)構(gòu)的,比如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結(jié)構(gòu)的數(shù)據(jù)。整體上就是Key-(Key-Value),我們可以理解為myhash是一個Hash表的名字,filed1,filed2是myhash這張Hash表中的鍵值,而myhash同時也是Redis這張大Hash表中的一個Key。

同理,不管Value是什么類型,它都有一個Key,這個Key就是Redis本身Key-Vaule的Key。

好的,現(xiàn)在就可以去對照命令手冊使用Redis,什么,你還沒有Redis的環(huán)境!沒關(guān)系,Redis官方提供了一個 ==網(wǎng)頁版Redis體驗== 的供大家練手:

網(wǎng)頁版Redis體驗

Redis的安裝與配置

安裝

單機(jī)版的Redis的安裝非常簡單,Redis兼容的操作系統(tǒng)為:Linux, OSX, OpenBSD, NetBSD, FreeBSD。支持Big endian和Little endian 處理器架構(gòu), 支持64位和32位系統(tǒng)。

在Linux下的安裝過程,只需要make命令就可:

   % make

如果是32位系統(tǒng):

   % make 32bit

編譯后可以通過make test測試:

   % make test

如有問題,可參考官方編譯說明。

Redis官方?jīng)]有Windows版本,但是微軟實現(xiàn)了一個Windows版的Redis Server 。

配置

Redis的配置文件是自注釋的,寫的密密麻麻,含義非常詳細(xì),清楚。運行時,把配置路徑作為參數(shù)啟動,使配置生效:

redis>redis-server /opt/redis/redis.conf

總共的配置項目超過50項,不過,在非集群模式下,通常關(guān)注的配置項目只有這些:

- maxmemory [3000m] 最大使用內(nèi)存
- daemonize [yes|no] 是否后臺啟動
- dir [path] 持久化數(shù)據(jù)存放目錄
- requirepass [password] 登錄密碼
- save [seconds] [changes] 在多少秒內(nèi)有多少次寫操作,就刷入一次數(shù)據(jù)到磁盤
- appendonly [yes|no] 是否開啟APPEND ONLY模式,這也是一種持久化策略,下一節(jié)會介紹

這幾個常用配置都非常好理解,但是第一個maxmemory要需要注意,這個配置涉及到配置Eviction policies,更多內(nèi)容可以參考 LRU算法進(jìn)行緩存回收

也可以參考這一份不錯的中文配置說明,但是由于Redis發(fā)展非常迅速,所以生產(chǎn)環(huán)境中使用的配置項一定要對照官方說明。

比如Redis配置中的VM配置(虛擬內(nèi)存機(jī)制),很多文章還在提,但是這個配置其實已經(jīng)在不斷的發(fā)展中被廢棄了,這個配置的用意是VM機(jī)制將數(shù)據(jù)分頁存放,由Redis將訪問量較少的頁即冷數(shù)據(jù)swap到磁盤上,訪問多的頁面由磁盤自動換出到內(nèi)存中,棒棒噠,對吧?但是對于Redis這么一個小軟件,希望把存儲做成如同Oracle一樣的方式,具備自動淘汰冷熱數(shù)據(jù)功能,并且比Linux操作系統(tǒng)本身更加優(yōu)秀,太難了。

對于Linux系統(tǒng),在配置文件之外,還有一些配置需要考慮:

修改內(nèi)存分配策略,使系統(tǒng)請求分配內(nèi)存時,永遠(yuǎn)假裝還有足夠的內(nèi)存
echo 'vm.overcommit_memory = 1' >>/etc/sysctl.conf
然后執(zhí)行: sysctl vm.overcommit_memory=1

定義了系統(tǒng)中每一個端口最大的監(jiān)聽隊列的長度,這是個全局的參數(shù),默認(rèn)128。
echo 1024 > /proc/sys/net/core/somaxconn

禁用透明緩存頁
echo never > /sys/kernel/mm/transparent_hugepage/enabled

關(guān)于這部分的內(nèi)容,可以參考 Redis AdministrationRedis latency problems troubleshooting。

Redis 的數(shù)據(jù)持久化

理解Redis的數(shù)據(jù)持久化對于使用Redis特別重要,因為? 當(dāng)然是因為不能隨便把數(shù)據(jù)搞丟,還有什么比這更重要么!況且可配置持久化,也是很多用戶選擇Redis的重要原因。

Redis官方有一篇專門闡述其持久化的文檔,以及Redis開發(fā)者Salvatore針對這個問題撰寫的一篇長文《Redis 持久化解密》。

不管他們怎么說,其實歸納起來我們就想知道3個問題:

  • Redis的持久化是如何工作的?
  • 這樣工作對性能的影響有多大?
  • 我應(yīng)該如何選擇?

一般來說Redis的所有工作數(shù)據(jù)都在內(nèi)存中,這也是內(nèi)存數(shù)據(jù)庫的特點,持久化數(shù)據(jù)只是啟動時加載,或者作為災(zāi)備手段。前面已經(jīng)提到了Redis的持久化有兩種方式:RDB和AOF。

RDB持久化方式能夠在指定的時間間隔能對你的數(shù)據(jù)進(jìn)行快照存儲,它是一個非常緊湊的文件,它保存了某個時間點得數(shù)據(jù)集,非常適用于數(shù)據(jù)集的備份,比如你可以在每個小時報保存一下過去24小時內(nèi)的數(shù)據(jù),同時每天保存過去30天的數(shù)據(jù),這樣即使出了問題你也可以根據(jù)需求恢復(fù)到不同版本的數(shù)據(jù)集。

RDB的持久化方式被稱為快照,在默認(rèn)情況下,Redis將數(shù)據(jù)庫快照保存在名字為dump.rdb的二進(jìn)制文件中。你可以對 Redis 進(jìn)行設(shè)置, 讓它在“ N 秒內(nèi)數(shù)據(jù)集至少有 M 個改動”這一條件被滿足時, 自動保存一次數(shù)據(jù)集。你也可以通過調(diào)用 SAVE或者 BGSAVE , 手動讓 Redis 進(jìn)行數(shù)據(jù)集保存操作。

比如說, 以下設(shè)置會讓 Redis 在滿足“ 60 秒內(nèi)有至少有 1000 個鍵被改動”這一條件時, 自動保存一次數(shù)據(jù)集:

save 60 1000

AOF持久化方式記錄每次對服務(wù)器寫的操作,當(dāng)服務(wù)器重啟的時候會重新執(zhí)行這些命令來恢復(fù)原始的數(shù)據(jù),AOF命令以redis協(xié)議追加保存每次寫的操作到文件末尾。Redis還能對AOF文件進(jìn)行后臺重寫,使得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 多久才將數(shù)據(jù) fsync 到磁盤一次。有三種方式:

  • 每次有新命令追加到 AOF 文件時就執(zhí)行一次 fsync :非常慢,也非常安全
  • 每秒 fsync 一次:足夠快(和使用 RDB 持久化差不多),并且在故障時只會丟失 1 秒鐘的數(shù)據(jù)。
  • 從不 fsync :將數(shù)據(jù)交給操作系統(tǒng)來處理。更快,也更不安全的選擇。

官方推薦(并且也是默認(rèn))的措施為每秒 fsync 一次, 這種 fsync 策略可以兼顧速度和安全性。

有兩點需要注意:

  • 如果同時開啟兩種持久化方式,在這種情況下, 當(dāng)redis重啟的時候會優(yōu)先載入AOF文件來恢復(fù)原始的數(shù)據(jù),因為在通常情況下AOF文件保存的數(shù)據(jù)集要比RDB文件保存的數(shù)據(jù)集要完整。

  • RDB快照會被用于master -> slave同步。

優(yōu)缺點

RDB的優(yōu)點

  • RDB是一個非常緊湊的文件,它保存了某個時間點得數(shù)據(jù)集,非常適用于數(shù)據(jù)集的備份,比如你可以在每個小時報保存一下過去24小時內(nèi)的數(shù)據(jù),同時每天保存過去30天的數(shù)據(jù),這樣即使出了問題你也可以根據(jù)需求恢復(fù)到不同版本的數(shù)據(jù)集。

  • RDB是一個緊湊的單一文件,很方便傳送到另一個遠(yuǎn)端數(shù)據(jù)中心或者亞馬遜的S3(可能加密),非常適用于災(zāi)難恢復(fù)。

  • RDB在保存RDB文件時父進(jìn)程唯一需要做的就是fork出一個子進(jìn)程,接下來的工作全部由子進(jìn)程來做,父進(jìn)程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。

  • 與AOF相比,在恢復(fù)大的數(shù)據(jù)集的時候,RDB方式會更快一些。

RDB的缺點

  • 如果你希望在redis意外停止工作(例如電源中斷)的情況下丟失的數(shù)據(jù)最少的話,那么RDB不適合你。雖然你可以配置不同的save時間點(例如每隔5分鐘并且對數(shù)據(jù)集有100個寫的操作,是Redis要完整的保存整個數(shù)據(jù)集是一個比較繁重的工作,你通常會每隔5分鐘或者更久做一次完整的保存,萬一在Redis意外宕機(jī),你可能會丟失幾分鐘的數(shù)據(jù)。

  • RDB 需要經(jīng)常fork子進(jìn)程來保存數(shù)據(jù)集到硬盤上,當(dāng)數(shù)據(jù)集比較大的時候,fork的過程是非常耗時的,可能會導(dǎo)致Redis在一些毫秒級內(nèi)不能響應(yīng)客戶端的請求。如果數(shù)據(jù)集巨大并且CPU性能不是很好的情況下,這種情況會持續(xù)1秒,AOF也需要fork,但是你可以調(diào)節(jié)重寫日志文件的頻率來提高數(shù)據(jù)集的耐久度。

AOF 優(yōu)點

  • 使用AOF 會讓你的Redis更加耐久: 你可以使用不同的fsync策略:無fsync,每秒fsync,每次寫的時候fsync。使用默認(rèn)的每秒fsync策略,Redis的性能依然很好(fsync是由后臺線程進(jìn)行處理的,主線程會盡力處理客戶端請求),一旦出現(xiàn)故障,你最多丟失1秒的數(shù)據(jù)。

  • AOF文件是一個只進(jìn)行追加的日志文件,所以不需要寫入seek,即使由于某些原因(磁盤空間已滿,寫的過程中宕機(jī)等等)未執(zhí)行完整的寫入命令,你也也可使用redis-check-aof工具修復(fù)這些問題。

  • Redis 可以在 AOF 文件體積變得過大時,自動地在后臺對 AOF 進(jìn)行重寫: 重寫后的新 AOF 文件包含了恢復(fù)當(dāng)前數(shù)據(jù)集所需的最小命令集合。 整個重寫操作是絕對安全的,因為 Redis 在創(chuàng)建新 AOF 文件的過程中,會繼續(xù)將命令追加到現(xiàn)有的 AOF 文件里面,即使重寫過程中發(fā)生停機(jī),現(xiàn)有的 AOF 文件也不會丟失。 而一旦新 AOF 文件創(chuàng)建完畢,Redis 就會從舊 AOF 文件切換到新 AOF 文件,并開始對新 AOF 文件進(jìn)行追加操作。

  • AOF 文件有序地保存了對數(shù)據(jù)庫執(zhí)行的所有寫入操作, 這些寫入操作以 Redis 協(xié)議的格式保存, 因此 AOF 文件的內(nèi)容非常容易被人讀懂, 對文件進(jìn)行分析(parse)也很輕松。 導(dǎo)出(export) AOF 文件也非常簡單: 舉個例子, 如果你不小心執(zhí)行了 FLUSHALL 命令, 但只要 AOF 文件未被重寫, 那么只要停止服務(wù)器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重啟 Redis , 就可以將數(shù)據(jù)集恢復(fù)到 FLUSHALL 執(zhí)行之前的狀態(tài)。

AOF 缺點

  • 對于相同的數(shù)據(jù)集來說,AOF 文件的體積通常要大于 RDB 文件的體積。

  • 根據(jù)所使用的 fsync 策略,AOF 的速度可能會慢于 RDB 。 在一般情況下, 每秒 fsync 的性能依然非常高, 而關(guān)閉 fsync 可以讓 AOF 的速度和 RDB 一樣快, 即使在高負(fù)荷之下也是如此。 不過在處理巨大的寫入載入時,RDB 可以提供更有保證的最大延遲時間(latency)。

如何選擇

關(guān)于如何選擇,官方這么說:

一般來說, 如果想達(dá)到足以媲美 PostgreSQL 的數(shù)據(jù)安全性, 你應(yīng)該同時使用兩種持久化功能。
如果你非常關(guān)心你的數(shù)據(jù), 但仍然可以承受數(shù)分鐘以內(nèi)的數(shù)據(jù)丟失, 那么你可以只使用 RDB 持久化。
有很多用戶都只使用 AOF 持久化, 但我們并不推薦這種方式: 因為定時生成 RDB 快照(snapshot)非常便于進(jìn)行數(shù)據(jù)庫備份, 并且 RDB 恢復(fù)數(shù)據(jù)集的速度也要比 AOF 恢復(fù)的速度要快, 除此之外, 使用 RDB 還可以避免之前提到的 AOF 程序的 bug 。
注意: 因為以上提到的種種原因, 未來我們可能會將 AOF 和 RDB 整合成單個持久化模型。

Two More Things ^_^

首先,官方所謂“未來我們可能會將 AOF 和 RDB 整合成單個持久化模型”在某種程度上已經(jīng)在4.0版本中實現(xiàn)了,這個改進(jìn)可以分為兩個層次來看:其一是AOF的實現(xiàn)機(jī)制,導(dǎo)致AOF文件太大,4.0 可以配置AOF,使其僅僅進(jìn)行增量記錄;其二是集群下主備必須全量復(fù)制,這種機(jī)制被更改為稱為PSYNC2.0的帶標(biāo)簽復(fù)制,看到Salvatore給五年前的一個留言的回復(fù),我想他那刻的內(nèi)心必是喜悅的(被你們TM懟了5年了啦)。

Reply4FiveYears

其次,我們回過頭來,討論一下到底什么叫數(shù)據(jù)持久化。非常簡化的來看,數(shù)據(jù)持久化可以分為這么5步:

  1. 客戶端發(fā)送一個寫命令到數(shù)據(jù)庫(數(shù)據(jù)在客戶端的內(nèi)存中)。
  2. 數(shù)據(jù)庫接收到這個寫命令(數(shù)據(jù)在服務(wù)器的內(nèi)存中)。
  3. 數(shù)據(jù)庫調(diào)用系統(tǒng)調(diào)用把寫數(shù)據(jù)存入磁盤(數(shù)據(jù)在內(nèi)核緩沖區(qū)kernel's buffer)
  4. 操作系統(tǒng)把寫緩沖區(qū)數(shù)據(jù)傳輸?shù)酱疟P控制器(數(shù)據(jù)在磁盤緩存中)
  5. 磁盤控制器真正把數(shù)據(jù)寫到物理介質(zhì)上。

傳統(tǒng)的Unix系統(tǒng)(Linux)實現(xiàn)在內(nèi)核中沒有緩沖區(qū)高速緩存或頁高速緩存,大多數(shù)磁盤I/O都通過緩沖區(qū)進(jìn)行。當(dāng)我們向文件寫入數(shù)據(jù)時(比如 POSIX API 的write系統(tǒng)調(diào)用),內(nèi)核通常先將數(shù)據(jù)復(fù)制到緩沖區(qū)中,然后排入隊列,晚些時候再寫入磁盤。這種方式被稱為延遲寫。通常,當(dāng)內(nèi)核需要重用緩沖區(qū)來存放其他磁盤數(shù)據(jù)時,他會把所有延遲寫數(shù)據(jù)寫入磁盤。為保證磁盤上實際文件與緩沖區(qū)中的內(nèi)容一致,Unix系統(tǒng)提供了sync,fsync和fdatasync三個函數(shù)。

其中,sync與fsync的區(qū)別在于,sync只是將所有修改過的塊緩沖區(qū)排入寫隊列,然后返回,它并不等待實際寫磁盤操作結(jié)束,一般update系統(tǒng)守護(hù)進(jìn)程會周期性調(diào)用sync函數(shù)(Linux是30s),fsync函數(shù)需要傳入文件描述符,它會等到磁盤寫操作結(jié)束才返回??梢韵胍姡m然每個操作都調(diào)用fsync是最保險的做法,但是這種大量隨機(jī)尋址對于任何運行于Rotational disks的應(yīng)用來說,都是非常慢的。

上述內(nèi)容旨在讓讀者理解數(shù)據(jù)持久化為什么需要各種策略,以及各種策略的意義,都是點到為止。

Redis集群

這一節(jié),我們會談?wù)揜edis的集群,。集群簡單的理解就是一堆機(jī)器齊心協(xié)力提供某種類型的服務(wù)。對于Redis集群,我們關(guān)心這么幾個內(nèi)容:數(shù)據(jù)是如何分布的,消息是如何傳遞的,出現(xiàn)異常的如何應(yīng)對。Redis集群研究和實踐(基于redis 3.0.5)這篇文章根據(jù)官方的安裝指南,記錄了非常詳細(xì)的安裝和操作步驟,本文就不在贅述了,我們主要重點理解一下前面提到的三個問題。

一致性哈希

之所以要介紹這個概念,是因為一致性哈希是Web Cache類系統(tǒng),用于數(shù)據(jù)分布最典型的設(shè)計(Memcached使用一致性哈希),我們將在后面一節(jié)與Redis的哈希槽(hash slot)進(jìn)行一個比較。

一致性哈希在Wiki上講的非常清楚:

需求

在使用n臺緩存服務(wù)器時,一種常用的負(fù)載均衡方式是,對資源o的請求使用hash(o) = o mod n來映射到某一臺緩存服務(wù)器。當(dāng)增加或減少一臺緩存服務(wù)器時這種方式可能會改變所有資源對應(yīng)的hash值,也就是所有的緩存都失效了,這會使得緩存服務(wù)器大量集中地向原始內(nèi)容服務(wù)器更新緩存。因些需要一致哈希算法來避免這樣的問題。

一致哈希盡可能使同一個資源映射到同一臺緩存服務(wù)器。這種方式要求增加一臺緩存服務(wù)器時,新的服務(wù)器盡量分擔(dān)存儲其他所有服務(wù)器的緩存資源。減少一臺緩存服務(wù)器時,其他所有服務(wù)器也可以盡量分擔(dān)存儲它的緩存資源。 一致哈希算法的主要思想是將每個緩存服務(wù)器與一個或多個哈希值域區(qū)間關(guān)聯(lián)起來,其中區(qū)間邊界通過計算緩存服務(wù)器對應(yīng)的哈希值來決定。(定義區(qū)間的哈希函數(shù)不一定和計算緩存服務(wù)器哈希值的函數(shù)相同,但是兩個函數(shù)的返回值的范圍需要匹配。)如果一個緩存服務(wù)器被移除,則它會從對應(yīng)的區(qū)間會被并入到鄰近的區(qū)間,其他的緩存服務(wù)器不需要任何改變。

也許上個圖,更容易理解:

一致性哈希

實現(xiàn)

一致哈希將每個對象映射到圓環(huán)邊上的一個點,系統(tǒng)再將可用的節(jié)點機(jī)器映射到圓環(huán)的不同位置。查找某個對象對應(yīng)的機(jī)器時,需要用一致哈希算法計算得到對象對應(yīng)圓環(huán)邊上位置,沿著圓環(huán)邊上查找直到遇到某個節(jié)點機(jī)器,這臺機(jī)器即為對象應(yīng)該保存的位置。

當(dāng)刪除一臺節(jié)點機(jī)器時,這臺機(jī)器上保存的所有對象都要移動到下一臺機(jī)器。添加一臺機(jī)器到圓環(huán)邊上某個點時,這個點的下一臺機(jī)器需要將這個節(jié)點前對應(yīng)的對象移動到新機(jī)器上。 更改對象在節(jié)點機(jī)器上的分布可以通過調(diào)整節(jié)點機(jī)器的位置來實現(xiàn)。

其實,也要不了幾行代碼

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哈希槽與數(shù)據(jù)分布

Redis采用的是哈希槽的機(jī)制,它通過函數(shù)hash(key) = CRC16(key)%16384將任意一個key映射到0-16383這個范圍,每個節(jié)點承接16384個key中的一段。如果增加或刪除一個節(jié)點,手動變更節(jié)點的承接范圍,具體的操作可以參考前面提到的《Redis集群研究和實踐》。

現(xiàn)在,我們再回到上面實現(xiàn)的這個一致性哈希,假設(shè)傳入了這樣一個hash函數(shù),并且手動對node添加后的節(jié)點承載范圍進(jìn)行調(diào)整:

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的哈希槽方案。這樣的方案,簡單、粗暴、直接并且有效,不過就是要麻煩你動動手去規(guī)劃。我認(rèn)為,一致性哈希的本質(zhì)就是通過在Hash->Node之間虛擬一個中間層使之變成Hash->RING Point->Node,從而避免Node增刪帶來的全局映射變動。從這個意義上說,Redis的哈希槽就是一個簡化版的一致性哈希方案。

這里我們不評價這種簡化版一致性哈希方案的優(yōu)劣,但它的確規(guī)避了系統(tǒng)對于節(jié)點增加或者刪除后,自動處理數(shù)據(jù)遷移,以及節(jié)點規(guī)劃給系統(tǒng)帶來的復(fù)雜性。

Gossip協(xié)議

Redis節(jié)點間的消息使用Gossip協(xié)議傳播,它常用于P2P的通信協(xié)議,這個協(xié)議就是模擬人類中傳播謠言的行為而來。

協(xié)議的核心內(nèi)容就是節(jié)點通過將信息隨機(jī)發(fā)送到N個節(jié)點來完成本次信息的傳播,其涉及到周期性、配對、交互模式。Gossip的交互模式分為兩種:Anti-entropy和Rumor mongering。

  • Anti-entropy:每個節(jié)點周期性地隨機(jī)選擇其他節(jié)點,然后通過相互交換自己的所有數(shù)據(jù)來消除兩者之間的差異。
  • Rumor mongering:當(dāng)一個節(jié)點有來新信息后,該節(jié)點變成活躍狀態(tài),并周期性地聯(lián)系其他節(jié)點向其發(fā)送新信息。

每個節(jié)點維護(hù)一個自己的信息表<key, (value, version)>,即屬性的值以及版本號;和一個記錄其他節(jié)點的信息表<node, <key, (value, version)>>。每個節(jié)點和系統(tǒng)中的某個節(jié)點相互配對成為peer。而節(jié)點的信息交換方式主要有3種。

  • Push:擁有狀態(tài)新信息的節(jié)點隨機(jī)選擇聯(lián)系節(jié)點并想起發(fā)送自己得到信息。
  • Pull:發(fā)起信息交換的節(jié)點隨機(jī)選擇聯(lián)系節(jié)點并從對方獲取信息。
  • Push-Pull混合模式:發(fā)起信息交換的節(jié)點向選擇的節(jié)點發(fā)送信息。

可以證明Gossip協(xié)議的傳播次數(shù)是收斂的。

傳播起來整個Redis集群內(nèi)部一共有N*(N-1)條傳輸路徑,路徑真的實在太多了,以至于開發(fā)者畫出來的圖都少了兩條(紅線補(bǔ)齊),就大概就像這個樣子:

Gossip協(xié)議

和Server與Client的不一樣,Redis內(nèi)部節(jié)點間采用的是二進(jìn)制協(xié)議以優(yōu)化帶寬。Redis節(jié)點間的“謠言”,大概是這個樣子的:

Gossip內(nèi)容

部分故障

對于一個分布式系統(tǒng),最大的挑戰(zhàn)就是要是節(jié)點掛了怎么辦,或者更具體的說如何知道一個節(jié)點是不是真的掛了,這也就是所謂的分布式系統(tǒng)的本質(zhì)困難:“partial failure(部分故障)”。但是不得不說,Redis的實現(xiàn)弱化了這個困難,因為它沒有提供通常意義上說的高可用性。

當(dāng)Redis集群中的一個主節(jié)點掛了之后,Goosip協(xié)議會選擇一個備節(jié)點替換上來,如果沒有備節(jié)點,整個集群系統(tǒng)就不可用了。是的,整體不可用!

這樣的設(shè)計避免了數(shù)據(jù)遷移和數(shù)據(jù)分布自動平衡,也避免了部分可用性需要進(jìn)行的一些屏蔽和邏輯阻斷。

具體來說,Redis的每個節(jié)點都擁有一個與其他節(jié)點相關(guān)的狀態(tài)標(biāo)示。有兩種狀態(tài)是用于失?。ㄊВz測的:PFAIL標(biāo)示和FAIL標(biāo)示。 PFAIL意味著可能失敗,這一個還沒有得到確認(rèn)的失敗類型。FAIL意味著一個節(jié)點失敗已經(jīng)在一個固定的時間范圍內(nèi)被大多數(shù)主節(jié)點確認(rèn)。

PFAIL 被確認(rèn)為FAIL 需要滿足下面這些條件:

  • A節(jié)點已經(jīng)將B節(jié)點標(biāo)示為PFAIL。
  • 節(jié)點A通過gossip收集了集群中大多數(shù)主節(jié)點關(guān)于B的狀態(tài)記錄。
  • 這些大多數(shù)的節(jié)點已經(jīng)在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 這個時間范圍內(nèi)將B標(biāo)記為PFAIL 或者FAIL 。

如果上述條件為真,那么節(jié)點A將做如下兩個動作:

  • 標(biāo)記B節(jié)點為 FAIL。
  • 把這個 FAIL 消息發(fā)送給其它所有可達(dá)的節(jié)點。

當(dāng)然關(guān)于Redis的失敗檢測,還有更細(xì)節(jié)的內(nèi)容和更復(fù)雜的情況,上面沒有提到,感興趣的讀者可以閱讀Redis集群規(guī)范。需要注意的是,FAIL標(biāo)識只是備節(jié)點提升為主節(jié)點的一個啟動條件。

節(jié)點選舉

備節(jié)點選舉和提升是備節(jié)點來處理的,并且需要主節(jié)點進(jìn)行選舉。一個備節(jié)點選舉發(fā)生在一個主節(jié)點被它的至少一個備節(jié)點標(biāo)記為FAIL狀態(tài),并且這些備節(jié)點具備成為主節(jié)點的先決條件下。

一個備節(jié)點為了把自己提升為主節(jié)點,它需要發(fā)起一輪選舉并且獲勝。一個主節(jié)點的所有備節(jié)點都可以在這個主節(jié)點處于FAIL狀態(tài)下發(fā)起選舉,然而最后只有一個備節(jié)點能夠贏得選舉并提升自己成為主節(jié)點。

一個備節(jié)點發(fā)起一輪選舉必須滿足下面這些條件:

  • 它的主節(jié)點處于FAIL狀態(tài)。
  • 這個主節(jié)點承載了非零數(shù)量的哈希槽。
  • 備節(jié)點與主節(jié)點的失聯(lián)時間在一個范圍內(nèi),這是為了確保備節(jié)點的數(shù)據(jù)足夠近,這個時間用戶可配置。

為了被選中,對于一個備節(jié)點來說,第一步就是增加自己的 currentEpoch計數(shù),并且從主節(jié)點實例請求選票。

備節(jié)點通過廣播一個FAILOVER_AUTH_REQUEST包給每個主節(jié)點來請求選票。然后,它等待一個最大 NODE_TIMEOUT*2(至少2秒)的時間接受回復(fù)。

一旦一個主節(jié)點投票給一個備節(jié)點,它主動回復(fù)一個FAILOVER_AUTH_ACK,它不能NODE_TIMEOUT * 2時間范圍內(nèi)再給這個備節(jié)點的競爭對手投票。這不是必須的安全性保障,但是對于阻止多個備節(jié)點同時選上非常有用。

一個備節(jié)點會丟棄發(fā)送選舉請求后,小于當(dāng)前 currentEpoch周期的所有AUTH_ACK回復(fù)。這確保了避免它錯誤地把上一輪選舉記票記到當(dāng)前周期。

一旦一個備節(jié)點得到大多數(shù)主節(jié)點的ACKs,它就贏得了選舉。另外,如果這個大多數(shù)主節(jié)點在NODE_TIMEOUT*2(至少2秒)時間內(nèi)沒有達(dá)到,當(dāng)前選舉會被廢棄,并且在NODE_TIMEOUT * 4(至少4秒)時間后,嘗試開始一輪新的選舉。

Redis集群方案對比

關(guān)于不同的集群方案對比,阿里云有一篇軟文做了一些介紹,我認(rèn)為:隨著Redis3.2.8的發(fā)布,Redis的集群已經(jīng)基本可以應(yīng)用于生產(chǎn)環(huán)境了。

關(guān)于不同集群對于高級功能的支持,軟文中有一個列表:

redis 4.0 阿里云redis codis
事務(wù) 支持相同slot 支持相同的slot 不支持
sub/pub 支持相同slot 支持 不支持
flushall 支持 支持 不支持
select 不支持 不支持 不支持
mset/mget 支持相同slot 支持 支持

以及性能對比:

不同集群性能對比

這篇軟文中說:

在實際生產(chǎn)環(huán)境中,使用原生的redis cluster,客戶端需要實現(xiàn)cluster protocol, 解析move, ask等指令并重定向節(jié)點,隨意訪問key可能需要兩次訪問操作才能完成,性能上并不能完全如單節(jié)點一樣。

實際對于java來說,Jedis是支持redis cluster的,在后面一個主題“Spring下使用Redis”,我們會發(fā)現(xiàn)除非節(jié)點出現(xiàn)變動,幾乎所有的客戶端命令都可以一次完成,所以可以認(rèn)為redis-cluster的性能就是實際應(yīng)用時的性能,真是1core頂人家8core?。?/p>

結(jié)語

關(guān)于Redis本身的內(nèi)容我們就聊到這里,希望這篇文章能給大家起一個拋磚引玉的作用。鑒于作者水平有限,如果大家覺得什么地方不對,歡迎提出來,大家一起學(xué)習(xí),一起進(jìn)步。

最后附上Books在《人月神話》中的一句話,這句話來自于書中“貴族專制、民主政治和系統(tǒng)設(shè)計 ( Aristocracy,
Democracy, and System Design)”一節(jié),是Redis作者Salvatore Sanfilippo的Google Group簽名,希望對你從一個側(cè)面理解Redis設(shè)計者的設(shè)計意圖:

If a system is to have conceptual integrity, someone must control the concepts.(如果要得到系統(tǒng)概念上的完整性, 那么必須有人控制這些概念)——— 《人月神話》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容