掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
本文從 Redis 基本特性入手,通過講述 Redis 的數(shù)據(jù)結(jié)構(gòu)和主要命令對 Redis 的基本能力進(jìn)行直觀介紹。之后概覽 Redis 提供的高級能力,并在部署、維護(hù)、性能調(diào)優(yōu)等多個(gè)方面深入介紹和指導(dǎo)。

十余年建站經(jīng)驗(yàn), 網(wǎng)站設(shè)計(jì)制作、網(wǎng)站建設(shè)客戶的見證與正確選擇。創(chuàng)新互聯(lián)提供完善的營銷型網(wǎng)頁建站明細(xì)報(bào)價(jià)表。后期開發(fā)更加便捷高效,我們致力于追求更美、更快、更規(guī)范。
本文適合使用 Redis 的普通開發(fā)人員,以及對 Redis 進(jìn)行選型、架構(gòu)設(shè)計(jì)和性能調(diào)優(yōu)的架構(gòu)設(shè)計(jì)人員:
Redis 是一個(gè)開源的,基于內(nèi)存的結(jié)構(gòu)化數(shù)據(jù)存儲媒介,可以作為數(shù)據(jù)庫、緩存服務(wù)或消息服務(wù)使用。
Redis 支持多種數(shù)據(jù)結(jié)構(gòu),包括字符串、哈希表、鏈表、集合、有序集合、位圖、Hyperloglogs 等。
Redis 具備 LRU 淘汰、事務(wù)實(shí)現(xiàn)、以及不同級別的硬盤持久化等能力,并且支持副本集和通過 Redis Sentinel 實(shí)現(xiàn)的高可用方案,同時(shí)還支持通過 Redis Cluster 實(shí)現(xiàn)的數(shù)據(jù)自動(dòng)分片能力。
Redis 的主要功能都基于單線程模型實(shí)現(xiàn),也就是說 Redis 使用一個(gè)線程來服務(wù)所有的客戶端請求,同時(shí) Redis 采用了非阻塞式 IO,并精細(xì)地優(yōu)化各種命令的算法時(shí)間復(fù)雜度。
這些信息意味著:
Redis 的數(shù)據(jù)結(jié)構(gòu)和相關(guān)常用命令
本節(jié)中將介紹 Redis 支持的主要數(shù)據(jù)結(jié)構(gòu),以及相關(guān)的常用 Redis 命令。本節(jié)只對 Redis 命令進(jìn)行扼要的介紹,且只列出了較常用的命令。
如果想要了解完整的 Redis 命令集,或了解某個(gè)命令的詳細(xì)使用方法,請參考官方文檔:
- https://redis.io/commands
Key
Redis 采用 Key-Value 型的基本數(shù)據(jù)結(jié)構(gòu),任何二進(jìn)制序列都可以作為 Redis 的 Key 使用(例如普通的字符串或一張 JPEG 圖片)。
關(guān)于 Key 的一些注意事項(xiàng):
String
String 是 Redis 的基礎(chǔ)數(shù)據(jù)類型,Redis 沒有 Int、Float、Boolean 等數(shù)據(jù)類型的概念,所有的基本類型在 Redis 中都以 String 體現(xiàn)。
與 String 相關(guān)的常用命令:
上文提到過,Redis 的基本數(shù)據(jù)類型只有 String,但 Redis 可以把 String 作為整型或浮點(diǎn)型數(shù)字來使用,主要體現(xiàn)在 INCR、DECR 類的命令上:
INCR/DECR 系列命令要求操作的 Value 類型為 String,并可以轉(zhuǎn)換為 64 位帶符號的整型數(shù)字,否則會(huì)返回錯(cuò)誤。
也就是說,進(jìn)行 INCR/DECR 系列命令的 Value,必須在 [-2^63 ~ 2^63 – 1] 范圍內(nèi)。
前文提到過,Redis 采用單線程模型,天然是線程安全的,這使得 INCR/DECR 命令可以非常便利的實(shí)現(xiàn)高并發(fā)場景下的精確控制。
例 1:庫存控制
在高并發(fā)場景下實(shí)現(xiàn)庫存余量的精準(zhǔn)校驗(yàn),確保不出現(xiàn)超賣的情況。設(shè)置庫存總量:
- SET inv:remain "100"
庫存扣減+余量校驗(yàn):
- DECR inv:remain
當(dāng) DECR 命令返回值大于等于 0 時(shí),說明庫存余量校驗(yàn)通過,如果返回小于 0 的值,則說明庫存已耗盡。
假設(shè)同時(shí)有 300 個(gè)并發(fā)請求進(jìn)行庫存扣減,Redis 能夠確保這 300 個(gè)請求分別得到 99 到 -200 的返回值,每個(gè)請求得到的返回值都是唯一的,絕對不會(huì)出現(xiàn)兩個(gè)請求得到一樣的返回值的情況。
例 2:自增序列生成
實(shí)現(xiàn)類似于 RDBMS 的 Sequence 功能,生成一系列唯一的序列號。設(shè)置序列起始值:
- SET sequence "10000"
獲取一個(gè)序列值:
- INCR sequence
直接將返回值作為序列使用即可。獲取一批(如100個(gè))序列值:
- INCRBY sequence 100
假設(shè)返回值為 N,那么 [N – 99 ~ N] 的數(shù)值都是可用的序列值。
當(dāng)多個(gè)客戶端同時(shí)向 Redis 申請自增序列時(shí),Redis 能夠確保每個(gè)客戶端得到的序列值或序列范圍都是全局唯一的,絕對不會(huì)出現(xiàn)不同客戶端得到了重復(fù)的序列值的情況。
List
Redis 的 List 是鏈表型的數(shù)據(jù)結(jié)構(gòu),可以使用 LPUSH/RPUSH/LPOP/RPOP 等命令在 List 的兩端執(zhí)行插入元素和彈出元素的操作。
雖然 List 也支持在特定 Index 上插入和讀取元素的功能,但其時(shí)間復(fù)雜度較高(O(N)),應(yīng)小心使用。
與 List 相關(guān)的常用命令:
應(yīng)盡可能控制一次獲取的元素?cái)?shù)量,一次獲取過大范圍的 List 元素會(huì)導(dǎo)致延遲,同時(shí)對長度不可預(yù)知的 List,避免使用 LRANGE key 0 -1 這樣的完整遍歷操作。
應(yīng)謹(jǐn)慎使用的 List 相關(guān)命令:
如果指定的元素不存在,返回 -1。如果指定 Key 不存在,不會(huì)進(jìn)行任何操作,時(shí)間復(fù)雜度 O(N)。
由于 Redis 的 List 是鏈表結(jié)構(gòu)的,上述的三個(gè)命令的算法效率較低,需要對 List 進(jìn)行遍歷,命令的耗時(shí)無法預(yù)估,在 List 長度大的情況下耗時(shí)會(huì)明顯增加,應(yīng)謹(jǐn)慎使用。
換句話說,Redis 的 List 實(shí)際是設(shè)計(jì)來用于實(shí)現(xiàn)隊(duì)列,而不是用于實(shí)現(xiàn)類似 ArrayList 這樣的列表的。
如果你不是想要實(shí)現(xiàn)一個(gè)雙端出入的隊(duì)列,那么請盡量不要使用 Redis 的 List 數(shù)據(jù)結(jié)構(gòu)。
為了更好支持隊(duì)列的特性,Redis 還提供了一系列阻塞式的操作命令,如 BLPOP/BRPOP 等,能夠?qū)崿F(xiàn)類似于 BlockingQueue 的能力,即在 List 為空時(shí),阻塞該連接,直到 List 中有對象可以出隊(duì)時(shí)再返回。
針對阻塞類的命令,此處不做詳細(xì)探討,請參考官方文檔中”Blocking operations on lists”一節(jié):
- https://redis.io/topics/data-types-intro
Hash
Hash 即哈希表,Redis 的 Hash 和傳統(tǒng)的哈希表一樣,是一種 field-value 型的數(shù)據(jù)結(jié)構(gòu),可以理解成將 HashMap 搬入 Redis。
Hash 非常適合用于表現(xiàn)對象類型的數(shù)據(jù),用 Hash 中的 field 對應(yīng)對象的 field 即可。
Hash 的優(yōu)點(diǎn)包括:
應(yīng)謹(jǐn)慎使用的 Hash 相關(guān)命令:
上述三個(gè)命令都會(huì)對 Hash 進(jìn)行完整遍歷,Hash 中的 field 數(shù)量與命令的耗時(shí)線性相關(guān),對于尺寸不可預(yù)知的 Hash,應(yīng)嚴(yán)格避免使用上面三個(gè)命令。
而改為使用 HSCAN 命令進(jìn)行游標(biāo)式的遍歷,具體請見:
- https://redis.io/commands/scan
Set
Redis Set 是無序的,不可重復(fù)的 String 集合。與 Set 相關(guān)的常用命令:
慎用的 Set 相關(guān)命令:
上述幾個(gè)命令涉及的計(jì)算量大,應(yīng)謹(jǐn)慎使用,特別是在參與計(jì)算的 Set 尺寸不可知的情況下,應(yīng)嚴(yán)格避免使用。
可以考慮通過 SSCAN 命令遍歷獲取相關(guān) Set 的全部 Member,具體請見:
- https://redis.io/commands/scan
如果需要做并集/交集/差集計(jì)算,可以在客戶端進(jìn)行,或在不服務(wù)實(shí)時(shí)查詢請求的 Slave 上進(jìn)行。
Sorted Set
Redis Sorted Set 是有序的、不可重復(fù)的 String 集合。Sorted Set 中的每個(gè)元素都需要指派一個(gè)分?jǐn)?shù)(score),Sorted Set 會(huì)根據(jù) Score 對元素進(jìn)行升序排序。
如果多個(gè) Member 擁有相同的 Score,則以字典序進(jìn)行升序排序。Sorted Set 非常適合用于實(shí)現(xiàn)排名。
Sorted Set 的主要命令:
慎用的 Sorted Set 相關(guān)命令:
ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范圍/指定 Score 范圍內(nèi)的所有 Member。時(shí)間復(fù)雜度 O(log(N)+M)。
上述幾個(gè)命令,應(yīng)盡量避免傳遞 [0 -1] 或 [-inf +inf] 這樣的參數(shù),來對 Sorted Set 做一次性的完整遍歷,特別是在 Sorted Set 的尺寸不可預(yù)知的情況下。
可以通過 ZSCAN 命令來進(jìn)行游標(biāo)式的遍歷,具體請見:
- https://redis.io/commands/scan
或通過 LIMIT 參數(shù)來限制返回 Member 的數(shù)量(適用于 ZRANGEBYSCORE 和 ZREVRANGEBYSCORE 命令),以實(shí)現(xiàn)游標(biāo)式的遍歷。
Bitmap 和 HyperLogLog
Redis 的這兩種數(shù)據(jù)結(jié)構(gòu)相較之前的并不常用,在本文中只做簡要介紹,如想要詳細(xì)了解這兩種數(shù)據(jù)結(jié)構(gòu)與其相關(guān)的命令,請參考官方文檔中的相關(guān)章節(jié):
- https://redis.io/topics/data-types-intro
Bitmap 在 Redis 中不是一種實(shí)際的數(shù)據(jù)類型,而是一種將 String 作為 Bitmap 使用的方法。
可以理解為將 String 轉(zhuǎn)換為 bit 數(shù)組。使用 Bitmap 來存儲 true/false 類型的簡單數(shù)據(jù)極為節(jié)省空間。
HyperLogLogs 是一種主要用于數(shù)量統(tǒng)計(jì)的數(shù)據(jù)結(jié)構(gòu),它和 Set 類似,維護(hù)一個(gè)不可重復(fù)的 String 集合,但是 HyperLogLogs 并不維護(hù)具體的 Member 內(nèi)容,只維護(hù) Member 的個(gè)數(shù)。
也就是說,HyperLogLogs 只能用于計(jì)算一個(gè)集合中不重復(fù)的元素?cái)?shù)量,所以它比 Set 要節(jié)省很多內(nèi)存空間。
其他常用命令:
使用 RENAMENX 時(shí),如果 Newkey 已經(jīng)存在,則不會(huì)進(jìn)行任何操作,時(shí)間復(fù)雜度 O(1)。
數(shù)據(jù)持久化
Redis 提供了將數(shù)據(jù)定期自動(dòng)持久化至硬盤的能力,包括 RDB 和 AOF 兩種方案,兩種方案分別有其長處和短板,可以配合起來同時(shí)運(yùn)行,確保數(shù)據(jù)的穩(wěn)定性。
必須使用數(shù)據(jù)持久化嗎?Redis 的數(shù)據(jù)持久化機(jī)制是可以關(guān)閉的。如果你只把 Redis 作為緩存服務(wù)使用,Redis 中存儲的所有數(shù)據(jù)都不是該數(shù)據(jù)的主體而僅僅是同步過來的備份,那么可以關(guān)閉 Redis 的數(shù)據(jù)持久化機(jī)制。
但通常來說,仍然建議至少開啟 RDB 方式的數(shù)據(jù)持久化,因?yàn)椋?/p>
這省去了手工從其他數(shù)據(jù)源(如 DB)同步數(shù)據(jù)的過程,而且要比其他任何的數(shù)據(jù)恢復(fù)方式都要快。
RDB
采用 RDB 持久方式,Redis 會(huì)定期保存數(shù)據(jù)快照至一個(gè) RBD 文件中,并在啟動(dòng)時(shí)自動(dòng)加載 RDB 文件,恢復(fù)之前保存的數(shù)據(jù)。
可以在配置文件中配置 Redis 進(jìn)行快照保存的時(shí)機(jī):
- save [seconds] [changes]
意為在 [seconds] 秒內(nèi)如果發(fā)生了 [changes] 次數(shù)據(jù)修改,則進(jìn)行一次 RDB 快照保存,例如:
- save 60 100
會(huì)讓 Redis 每 60 秒檢查一次數(shù)據(jù)變更情況,如果發(fā)生了 100 次或以上的數(shù)據(jù)變更,則進(jìn)行 RDB 快照保存??梢耘渲枚鄺l Save 指令,讓 Redis 執(zhí)行多級的快照保存策略。
Redis 默認(rèn)開啟 RDB 快照,默認(rèn)的 RDB 策略如下:
- save 900 1
- save 300 10
- save 60 10000
也可以通過 BGSAVE 命令手工觸發(fā) RDB 快照保存。
RDB 的優(yōu)點(diǎn):
RDB 的缺點(diǎn):
AOF
采用 AOF 持久方式時(shí),Redis 會(huì)把每一個(gè)寫請求都記錄在一個(gè)日志文件里。在 Redis 重啟時(shí),會(huì)把 AOF 文件中記錄的所有寫操作順序執(zhí)行一遍,確保數(shù)據(jù)恢復(fù)到最新。
AOF 默認(rèn)是關(guān)閉的,如要開啟,進(jìn)行如下配置:
- appendonly yes
AOF 提供了三種 Fsync 配置,always/everysec/no,通過配置項(xiàng) [appendfsync] 指定:
隨著 AOF 不斷地記錄寫操作日志,必定會(huì)出現(xiàn)一些無用的日志,例如某個(gè)時(shí)間點(diǎn)執(zhí)行了命令 SET key1 “abc”,在之后某個(gè)時(shí)間點(diǎn)又執(zhí)行了 SET key1 “bcd”,那么第一條命令很顯然是沒有用的。
大量的無用日志會(huì)讓 AOF 文件過大,也會(huì)讓數(shù)據(jù)恢復(fù)的時(shí)間過長。所以 Redis 提供了 AOF Rewrite 功能,可以重寫 AOF 文件,只保留能夠把數(shù)據(jù)恢復(fù)到最新狀態(tài)的最小寫操作集。
AOF Rewrite 可以通過 BGREWRITEAOF 命令觸發(fā),也可以配置 Redis 定期自動(dòng)進(jìn)行:
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
上面兩行配置的含義是,Redis 在每次 AOF Rewrite 時(shí),會(huì)記錄完成 Rewrite 后的 AOF 日志大小,當(dāng) AOF 日志大小在該基礎(chǔ)上增長了 100% 后,自動(dòng)進(jìn)行 AOF Rewrite。
同時(shí)如果增長的大小沒有達(dá)到 64MB,則不會(huì)進(jìn)行 Rewrite。
AOF 的優(yōu)點(diǎn):
AOF 的缺點(diǎn):
內(nèi)存管理與數(shù)據(jù)淘汰機(jī)制
最大內(nèi)存設(shè)置
默認(rèn)情況下,在 32 位 OS 中,Redis 最大使用 3GB 的內(nèi)存,在 64 位 OS 中則沒有限制。
在使用 Redis 時(shí),應(yīng)該對數(shù)據(jù)占用的最大空間有一個(gè)基本準(zhǔn)確的預(yù)估,并為 Redis 設(shè)定最大使用的內(nèi)存。
否則在 64 位 OS 中 Redis 會(huì)無限制地占用內(nèi)存(當(dāng)物理內(nèi)存被占滿后會(huì)使用 Swap 空間),容易引發(fā)各種各樣的問題。
通過如下配置控制 Redis 使用的最大內(nèi)存:
- maxmemory 100mb
在內(nèi)存占用達(dá)到了 maxmemory 后,再向 Redis 寫入數(shù)據(jù)時(shí),Redis 會(huì):
在為 Redis 設(shè)置 maxmemory 時(shí),需要注意:如果采用了 Redis 的主從同步,主節(jié)點(diǎn)向從節(jié)點(diǎn)同步數(shù)據(jù)時(shí),會(huì)占用掉一部分內(nèi)存空間;如果 maxmemory 過于接近主機(jī)的可用內(nèi)存,導(dǎo)致數(shù)據(jù)同步時(shí)內(nèi)存不足。
所以設(shè)置的 maxmemory 不要過于接近主機(jī)可用的內(nèi)存,留出一部分預(yù)留用作主從同步。
數(shù)據(jù)淘汰機(jī)制
Redis 提供了 5 種數(shù)據(jù)淘汰策略:
最好為 Redis 指定一種有效的數(shù)據(jù)淘汰策略以配合 maxmemory 設(shè)置,避免在內(nèi)存使用滿后發(fā)生寫入失敗的情況。
一般來說,推薦使用的策略是 volatile-lru,并辨識 Redis 中保存的數(shù)據(jù)的重要性。
對于那些重要的,絕對不能丟棄的數(shù)據(jù)(如配置類數(shù)據(jù)等),應(yīng)不設(shè)置有效期,這樣 Redis 就永遠(yuǎn)不會(huì)淘汰這些數(shù)據(jù)。
對于那些相對不是那么重要的,并且能夠熱加載的數(shù)據(jù)(比如緩存最近登錄的用戶信息,當(dāng)在 Redis 中找不到時(shí),程序會(huì)去 DB 中讀取),可以設(shè)置上有效期,這樣在內(nèi)存不夠時(shí) Redis 就會(huì)淘汰這部分?jǐn)?shù)據(jù)。
配置方法:
- maxmemory-policy volatile-lru #默認(rèn)是noeviction,即不進(jìn)行數(shù)據(jù)淘汰
Pipelining
Redis 提供許多批量操作的命令,如 MSET/MGET/HMSET/HMGET 等等,這些命令存在的意義是減少維護(hù)網(wǎng)絡(luò)連接和傳輸數(shù)據(jù)所消耗的資源和時(shí)間。
例如連續(xù)使用 5 次 SET 命令設(shè)置 5 個(gè)不同的 Key,比起使用一次 MSET 命令設(shè)置 5 個(gè)不同的 Key,效果是一樣的,但前者會(huì)消耗更多的 RTT(Round Trip Time)時(shí)長,永遠(yuǎn)應(yīng)優(yōu)先使用后者。
然而,如果客戶端要連續(xù)執(zhí)行的多次操作無法通過 Redis 命令組合在一起,例如:
- SET a "abc"
- INCR b
- HSET c name "hi"
此時(shí)便可以使用 Redis 提供的 Pipelining 功能來實(shí)現(xiàn)在一次交互中執(zhí)行多條命令。
使用 Pipelining 時(shí),只需要從客戶端一次向 Redis 發(fā)送多條命令(以 rn)分隔,Redis 就會(huì)依次執(zhí)行這些命令,并且把每個(gè)命令的返回按順序組裝在一起一次返回,比如:
- $ (printf "PINGrnPINGrnPINGrn"; sleep 1) | nc localhost 6379
- +PONG
- +PONG
- +PONG
大部分的 Redis 客戶端都對 Pipelining 提供支持,所以開發(fā)者通常并不需要自己手工拼裝命令列表。
Pipelining 的局限性
Pipelining 只能用于執(zhí)行連續(xù)且無相關(guān)性的命令,當(dāng)某個(gè)命令的生成需要依賴于前一個(gè)命令的返回時(shí),就無法使用 Pipelining 了。通過 Scripting 功能,可以規(guī)避這一局限性。
事務(wù)與 Scripting
Pipelining 能夠讓 Redis 在一次交互中處理多條命令,然而在一些場景下,我們可能需要在此基礎(chǔ)上確保這一組命令是連續(xù)執(zhí)行的。
比如獲取當(dāng)前累計(jì)的 PV 數(shù)并將其清 0:
- > GET vCount
- 12384
- > SET vCount 0
- OK
如果在 GET 和 SET 命令之間插進(jìn)來一個(gè) INCR vCount,就會(huì)使客戶端拿到的 vCount 不準(zhǔn)確。
Redis 的事務(wù)可以確保復(fù)數(shù)命令執(zhí)行時(shí)的原子性。也就是說 Redis 能夠保證:一個(gè)事務(wù)中的一組命令是絕對連續(xù)執(zhí)行的,在這些命令執(zhí)行完成之前,絕對不會(huì)有來自于其他連接的其他命令插進(jìn)去執(zhí)行。
通過 MULTI 和 EXEC 命令來把這兩個(gè)命令加入一個(gè)事務(wù)中:
- > MULTI
- OK
- > GET vCount
- QUEUED
- > SET vCount 0
- QUEUED
- > EXEC
- 1) 12384
- 2) OK
Redis 在接收到 MULTI 命令后便會(huì)開啟一個(gè)事務(wù),這之后的所有讀寫命令都會(huì)保存在隊(duì)列中但并不執(zhí)行,直到接收到 EXEC 命令后,Redis 會(huì)把隊(duì)列中的所有命令連續(xù)順序執(zhí)行,并以數(shù)組形式返回每個(gè)命令的返回結(jié)果。
可以使用 DISCARD 命令放棄當(dāng)前的事務(wù),將保存的命令隊(duì)列清空。需要注意的是,Redis 事務(wù)不支持回滾。
如果一個(gè)事務(wù)中的命令出現(xiàn)了語法錯(cuò)誤,大部分客戶端驅(qū)動(dòng)會(huì)返回錯(cuò)誤,2.6.5 版本以上的 Redis 也會(huì)在執(zhí)行 EXEC 時(shí)檢查隊(duì)列中的命令是否存在語法錯(cuò)誤,如果存在,則會(huì)自動(dòng)放棄事務(wù)并返回錯(cuò)誤。
但如果一個(gè)事務(wù)中的命令有非語法類的錯(cuò)誤(比如對 String 執(zhí)行 HSET 操作),無論客戶端驅(qū)動(dòng)還是 Redis 都無法在真正執(zhí)行這條命令之前發(fā)現(xiàn),所以事務(wù)中的所有命令仍然會(huì)被依次執(zhí)行。
在這種情況下,會(huì)出現(xiàn)一個(gè)事務(wù)中部分命令成功部分命令失敗的情況,然而與 RDBMS 不同,Redis 不提供事務(wù)回滾的功能,所以只能通過其他方法進(jìn)行數(shù)據(jù)的回滾。
通過事務(wù)實(shí)現(xiàn) CAS
Redis 提供了 WATCH 命令與事務(wù)搭配使用,實(shí)現(xiàn) CAS 樂觀鎖的機(jī)制。
假設(shè)要實(shí)現(xiàn)將某個(gè)商品的狀態(tài)改為已售:
- if(exec(HGET stock:1001 state) == "in stock")
- exec(HSET stock:1001 state "sold");
這一偽代碼執(zhí)行時(shí),無法確保并發(fā)安全性,有可能多個(gè)客戶端都獲取到了”in stock”的狀態(tài),導(dǎo)致一個(gè)庫存被售賣多次。
使用 WATCH 命令和事務(wù)可以解決這一問題:
- exec(WATCH stock:1001);
- if(exec(HGET stock:1001 state) == "in stock") {
- exec(MULTI);
- exec(HSET stock:1001 state "sold");
- exec(EXEC);
- }
WATCH 的機(jī)制是:在事務(wù) EXEC 命令執(zhí)行時(shí),Redis 會(huì)檢查被 WATCH 的 Key,只有被 WATCH 的 Key 從 WATCH 起始時(shí)至今沒有發(fā)生過變更,EXEC 才會(huì)被執(zhí)行。
如果 WATCH 的 Key 在 WATCH 命令到 EXEC 命令之間發(fā)生過變化,則 EXEC 命令會(huì)返回失敗。
Scripting
通過 EVAL 與 EVALSHA 命令,可以讓 Redis 執(zhí)行 LUA 腳本。這就類似于 RDBMS 的存儲過程一樣,可以把客戶端與 Redis 之間密集的讀/寫交互放在服務(wù)端進(jìn)行,避免過多的數(shù)據(jù)交互,提升性能。
Scripting 功能是作為事務(wù)功能的替代者誕生的,事務(wù)提供的所有能力 Scripting 都可以做到。Redis 官方推薦使用 LUA Script 來代替事務(wù),前者的效率和便利性都超過了事務(wù)。
關(guān)于 Scripting 的具體使用,本文不做詳細(xì)介紹,請參考官方文檔:
- https://redis.io/commands/eval
Redis 性能調(diào)優(yōu)
盡管 Redis 是一個(gè)非??焖俚膬?nèi)存數(shù)據(jù)存儲媒介,也并不代表 Redis 不會(huì)產(chǎn)生性能問題。
前文中提到過,Redis 采用單線程模型,所有的命令都是由一個(gè)線程串行執(zhí)行的,所以當(dāng)某個(gè)命令執(zhí)行耗時(shí)較長時(shí),會(huì)拖慢其后的所有命令,這使得 Redis 對每個(gè)任務(wù)的執(zhí)行效率更加敏感。
針對 Redis 的性能優(yōu)化,主要從下面幾個(gè)層面入手:
- echo never > /sys/kernel/mm/transparent_hugepage/enabled
考慮引入讀寫分離機(jī)制。
長耗時(shí)命令
Redis 絕大多數(shù)讀寫命令的時(shí)間復(fù)雜度都在 O(1) 到 O(N) 之間,在文本和官方文檔中均對每個(gè)命令的時(shí)間復(fù)雜度有說明。
通常來說,O(1) 的命令是安全的,O(N) 命令在使用時(shí)需要注意,如果 N 的數(shù)量級不可預(yù)知,則應(yīng)避免使用。
例如對一個(gè) field 數(shù)未知的 Hash 數(shù)據(jù)執(zhí)行 HGETALL/HKEYS/HVALS 命令,通常來說這些命令執(zhí)行的很快,但如果這個(gè) Hash 中的 field 數(shù)量極多,耗時(shí)就會(huì)成倍增長。
又如使用 SUNION 對兩個(gè) Set 執(zhí)行 Union 操作,或使用 SORT 對 List/Set 執(zhí)行排序操作等時(shí),都應(yīng)該嚴(yán)加注意。
避免在使用這些 O(N) 命令時(shí)發(fā)生問題主要有幾個(gè)辦法:
Redis 提供了 SCAN 命令,可以對 Redis 中存儲的所有 Key 進(jìn)行游標(biāo)式的遍歷,避免使用 Keys 命令帶來的性能問題。
同時(shí)還有 SSCAN/HSCAN/ZSCAN 等命令,分別用于對 Set/Hash/Sorted Set 中的元素進(jìn)行游標(biāo)式遍歷。
SCAN 類命令的使用請參考官方文檔:
- https://redis.io/commands/scan
Redis 提供了 Slow Log 功能,可以自動(dòng)記錄耗時(shí)較長的命令。相關(guān)的配置參數(shù)有兩個(gè):
- slowlog-log-slower-than xxxms #執(zhí)行時(shí)間慢于xxx毫秒的命令計(jì)入Slow Log
- slowlog-max-len xxx #Slow Log的長度,即最大紀(jì)錄多少條Slow Log
使用 SLOWLOG GET [number] 命令,可以輸出最近進(jìn)入 Slow Log 的 Mumber 條命令。使用 SLOWLOG RESET 命令,可以重置 Slow Log。
網(wǎng)絡(luò)引發(fā)的延遲
盡可能使用長連接或連接池,避免頻繁創(chuàng)建銷毀連接,客戶端進(jìn)行的批量數(shù)據(jù)操作,應(yīng)使用 Pipeline 特性在一次交互中完成。具體請參照本文的 Pipelining 章節(jié)。
數(shù)據(jù)持久化引發(fā)的延遲
Redis 的數(shù)據(jù)持久化工作本身就會(huì)帶來延遲,需要根據(jù)數(shù)據(jù)的安全級別和性能要求制定合理的持久化策略:
根據(jù)具體的情況合理配置 RDB 快照和 AOF Rewrite 時(shí)機(jī),避免過于頻繁的 Fork 帶來的延遲。
Redis 在 Fork 子進(jìn)程時(shí)需要將內(nèi)存分頁表拷貝至子進(jìn)程,以占用了 24GB 內(nèi)存的 Redis 實(shí)例為例,共需要拷貝 24GB / 4KB * 8 = 48MB 的數(shù)據(jù)。
在使用單 Xeon 2.27Ghz 的物理機(jī)上,這一 Fork 操作耗時(shí) 216ms??梢酝ㄟ^ INFO 命令返回的 latest_fork_usec 字段查看上一次 Fork 操作的耗時(shí)(微秒)。
Swap 引發(fā)的延遲
當(dāng) Linux 將 Redis 所用的內(nèi)存分頁移至 Swap 空間時(shí),將會(huì)阻塞 Redis 進(jìn)程,導(dǎo)致 Redis 出現(xiàn)不正常的延遲。
Swap 通常在物理內(nèi)存不足或一些進(jìn)程在進(jìn)行大量 I/O 操作時(shí)發(fā)生,應(yīng)盡可能避免上述兩種情況的出現(xiàn)。
/proc/ /smaps 文件中會(huì)保存進(jìn)程的 Swap 記錄,通過查看這個(gè)文件,能夠判斷 Redis 的延遲是否由 Swap 產(chǎn)生。
如果這個(gè)文件中記錄了較大的 Swap size,則說明延遲很有可能是 Swap 造成的。
數(shù)據(jù)淘汰引發(fā)的延遲
當(dāng)同一秒內(nèi)有大量 Key 過期時(shí),也會(huì)引發(fā) Redis 的延遲。在使用時(shí)應(yīng)盡量將 Key 的失效時(shí)間錯(cuò)開。
引入讀寫分離機(jī)制
Redis 的主從復(fù)制能力可以實(shí)現(xiàn)一主多從的多節(jié)點(diǎn)架構(gòu),在這一架構(gòu)下,主節(jié)點(diǎn)接收所有寫請求,并將數(shù)據(jù)同步給多個(gè)從節(jié)點(diǎn)。
在這一基礎(chǔ)上,我們可以讓從節(jié)點(diǎn)提供對實(shí)時(shí)性要求不高的讀請求服務(wù),以減小主節(jié)點(diǎn)的壓力。
尤其是針對一些使用了長耗時(shí)命令的統(tǒng)計(jì)類任務(wù),完全可以指定在一個(gè)或多個(gè)從節(jié)點(diǎn)上執(zhí)行,避免這些長耗時(shí)命令影響其他請求的響應(yīng)。
主從復(fù)制與集群分片
主從復(fù)制
Redis 支持一主多從的主從復(fù)制架構(gòu)。一個(gè) Master 實(shí)例負(fù)責(zé)處理所有的寫請求,Master 將寫操作同步至所有 Slave。
借助 Redis 的主從復(fù)制,可以實(shí)現(xiàn)讀寫分離和高可用:
啟用主從復(fù)制非常簡單,只需要配置多個(gè) Redis 實(shí)例,在作為 Slave 的 Redis 實(shí)例中配置:
- slaveof 192.168.1.1 6379 #指定Master的IP和端口
當(dāng) Slave 啟動(dòng)后,會(huì)從 Master 進(jìn)行一次冷啟動(dòng)數(shù)據(jù)同步,由 Master 觸發(fā) BGSAVE 生成 RDB 文件推送給 Slave 進(jìn)行導(dǎo)入。
導(dǎo)入完成后 Master 再將增量數(shù)據(jù)通過 Redis Protocol 同步給 Slave。之后主從之間的數(shù)據(jù)便一直以 Redis Protocol 進(jìn)行同步。
使用 Sentinel 做自動(dòng) Failover:Redis 的主從復(fù)制功能本身只是做數(shù)據(jù)同步,并不提供監(jiān)控和自動(dòng) Failover 能力,要通過主從復(fù)制功能來實(shí)現(xiàn) Redis 的高可用,還需要引入一個(gè)組件:Redis Sentinel。
Redis Sentinel 是 Redis 官方開發(fā)的監(jiān)控組件,可以監(jiān)控 Redis 實(shí)例的狀態(tài),通過 Master 節(jié)點(diǎn)自動(dòng)發(fā)現(xiàn) Slave 節(jié)點(diǎn),并在監(jiān)測到 Master 節(jié)點(diǎn)失效時(shí)選舉出一個(gè)新的 Master,并向所有 Redis 實(shí)例推送新的主從配置。
Redis Sentinel 需要至少部署 3 個(gè)實(shí)例才能形成選舉關(guān)系。關(guān)鍵配置:
- sentinel monitor mymaster 127.0.0.1 6379 2 #Master實(shí)例的IP、端口,以及選舉需要的贊成票數(shù)
- sentinel down-after-milliseconds mymaster 60000 #多長時(shí)間沒有響應(yīng)視為Master失效
- sentinel failover-timeout mymaster 180000 #兩次failover嘗試間的間隔時(shí)長
- sentinel parallel-syncs mymaster 1 #如果有多個(gè)Slave,可以通過此配置指定同時(shí)從新Master進(jìn)行數(shù)據(jù)同步的Slave數(shù),避免所有Slave同時(shí)進(jìn)行數(shù)據(jù)同步導(dǎo)致查詢服務(wù)也不可用
另外需要注意的是,Redis Sentinel 實(shí)現(xiàn)的自動(dòng) Failover 不是在同一個(gè) IP 和端口上完成的。
也就是說自動(dòng) Failover 產(chǎn)生的新 Master 提供服務(wù)的 IP 和端口與之前的 Master 是不一樣的。
所以要實(shí)現(xiàn) HA,還要求客戶端必須支持 Sentinel,能夠與 Sentinel 交互獲得新 Master 的信息才行。
集群分片
為何要做集群分片?原因如下:
當(dāng)上述兩個(gè)問題出現(xiàn)時(shí),就必須要對 Redis 進(jìn)行分片了。Redis 的分片方案有很多種,例如很多 Redis 的客戶端都自行實(shí)現(xiàn)了分片功能,也有向 Twemproxy 這樣的以代理方式實(shí)現(xiàn)的 Redis 分片方案。
然而選擇的方案還應(yīng)該是 Redis 官方在 3.0 版本中推出的 Redis Cluster 分片方案。
本文不會(huì)對 Redis Cluster 的具體安裝和部署細(xì)節(jié)進(jìn)行介紹,重點(diǎn)介紹 Redis Cluster 帶來的好處與弊端。
Redis Cluster 的能力:
其中第三點(diǎn)是基于主從復(fù)制來實(shí)現(xiàn)的,Redis Cluster 的每個(gè)數(shù)據(jù)分片都采用了主從復(fù)制的結(jié)構(gòu),原理和前文所述的主從復(fù)制完全一致。
唯一的區(qū)別是省去了 Redis Sentinel 這一額外的組件,由 Redis Cluster 負(fù)責(zé)進(jìn)行一個(gè)分片內(nèi)部的節(jié)點(diǎn)監(jiān)控和自動(dòng) Failover。
Redis Cluster 分片原理:Redis Cluster 中共有 16384 個(gè) hash slot,Redis 會(huì)計(jì)算每個(gè) Key 的 CRC16,將結(jié)果與 16384 取模,來決定該 Key 存儲在哪一個(gè) hash slot 中。
同時(shí)需要指定 Redis Cluster 中每個(gè)數(shù)據(jù)分片負(fù)責(zé)的 Slot 數(shù)。Slot 的分配在任何時(shí)間點(diǎn)都可以進(jìn)行重新分配。
客戶端在對 Key 進(jìn)行讀寫操作時(shí),可以連接 Cluster 中的任意一個(gè)分片,如果操作的 Key 不在此分片負(fù)責(zé)的 Slot 范圍內(nèi),Redis Cluster 會(huì)自動(dòng)將請求重定向到正確的分片上。
Hash Tags:在基礎(chǔ)的分片原則上,Redis 還支持 hash tags 功能,以 hash tags 要求的格式明明的 Key,將會(huì)確保進(jìn)入同一個(gè) Slot 中。
例如:{uiv}user:1000 和 {uiv}user:1001 擁有同樣的 hash tag {uiv},會(huì)保存在同一個(gè) Slot 中。
使用 Redis Cluster 時(shí),Pipelining、事務(wù)和 LUA Script 功能涉及的 Key 必須在同一個(gè)數(shù)據(jù)分片上,否則將會(huì)返回錯(cuò)誤。
如要在 Redis Cluster 中使用上述功能,就必須通過 hash tags 來確保一個(gè) Pipeline 或一個(gè)事務(wù)中操作的所有 Key 都位于同一個(gè) Slot 中。
有一些客戶端(如 Redisson)實(shí)現(xiàn)了集群化的 Pipelining 操作,可以自動(dòng)將一個(gè) Pipeline 里的命令按 Key 所在的分片進(jìn)行分組,分別發(fā)到不同的分片上執(zhí)行。
但是 Redis 不支持跨分片的事務(wù),事務(wù)和 LUA Script 還是必須遵循所有 Key 在一個(gè)分片上的規(guī)則要求。
主從復(fù)制 VS 集群分片
在設(shè)計(jì)軟件架構(gòu)時(shí),要如何在主從復(fù)制和集群分片兩種部署方案中取舍呢?從各個(gè)方面看,Redis Cluster 都是優(yōu)于主從復(fù)制的方案:
那是不是代表 Redis Cluster 永遠(yuǎn)是優(yōu)于主從復(fù)制的選擇呢?并不是。軟件架構(gòu)永遠(yuǎn)不是越復(fù)雜越好,復(fù)雜的架構(gòu)在帶來顯著好處的同時(shí),一定也會(huì)帶來相應(yīng)的弊端。
采用 Redis Cluster 的弊端包括:
同時(shí)在進(jìn)行分片的增減操作時(shí),還需要進(jìn)行 Reshard 操作,遠(yuǎn)比主從模式下增加一個(gè) Slave 的復(fù)雜度要高。
這就使得在開發(fā)時(shí)必須對相應(yīng)場景下涉及的 Key 進(jìn)行額外的規(guī)劃和規(guī)范要求。如果應(yīng)用的場景中大量涉及事務(wù)和 Script 的使用,如何在保證這兩個(gè)功能的正常運(yùn)作前提下把數(shù)據(jù)平均分到多個(gè)數(shù)據(jù)分片中就會(huì)成為難點(diǎn)。
所以說,在主從復(fù)制和集群分片兩個(gè)方案中做出選擇時(shí),應(yīng)該從應(yīng)用軟件的功能特性、數(shù)據(jù)和訪問量級、未來發(fā)展規(guī)劃等方面綜合考慮,只在確實(shí)有必要引入數(shù)據(jù)分片時(shí)再使用 Redis Cluster。
下面是一些建議:
綜合上面幾點(diǎn)考慮,如果單臺主機(jī)的可用物理內(nèi)存完全足以支撐對 Redis 的容量需求,且 Redis 面臨的并發(fā)寫壓力距離 Benchmark 值還尚有距離,建議采用主從復(fù)制的架構(gòu),可以省去很多不必要的麻煩。
同時(shí),如果應(yīng)用中大量使用 Pipelining 和事務(wù),也建議盡可能選擇主從復(fù)制架構(gòu),可以減少設(shè)計(jì)和開發(fā)時(shí)的復(fù)雜度。
Redis Java 客戶端的選擇
Redis 的 Java 客戶端很多,官方推薦的有三種:
在這里對 Jedis 和 Redisson 進(jìn)行對比介紹。
Jedis:
Redisson:
對于 Jedis 和 Redisson 的選擇,同樣應(yīng)遵循前述的原理,盡管 Jedis 比起 Redisson 有各種各樣的不足,但也應(yīng)該在需要使用 Redisson 的高級特性時(shí)再選用 Redisson,避免造成不必要的程序復(fù)雜度提升。
Jedis:
- github:https://github.com/xetorthio/jedis
- 文檔:https://github.com/xetorthio/jedis/wiki
Redisson:
- github:https://github.com/redisson/redisson
- 文檔:https://github.com/redisson/redisson/wiki

我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流