'你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)'

Redis 數據結構 數據庫 Java 算法 收藏 設計 編輯小猿 2019-08-21
"
"
你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)

目錄

  • 概述
  • Redis的數據結構和常用命令
  • 數據持久化
  • 內存管理與數據淘汰機制
  • Pipelining
  • 事務與Scripting
  • Redis性能調優
  • 主從複製與集群分片
  • Redis Java客戶端的選擇


本文將從Redis的基本特性入手,通過講述Redis的數據結構和主要命令對Redis的基本能力進行直觀介紹。

然後概覽Redis提供的高級能力,並在部署、維護、性能調優等多個方面進行更深入的介紹和指導。

本文適合使用Redis的普通開發人員,以及對Redis進行選型、架構設計和性能調優的架構設計人員。

概述

Redis是一個開源的,基於內存的結構化數據存儲媒介,可以作為數據庫、緩存服務或消息服務使用。

Redis支持多種數據結構,包括字符串、哈希表、鏈表、集合、有序集合、位圖、Hyperloglogs等。

Redis具備LRU淘汰、事務實現、以及不同級別的硬盤持久化等能力,並且支持副本集和通過Redis Sentinel實現的高可用方案,同時還支持通過Redis Cluster實現的數據自動分片能力。

Redis的主要功能都基於單線程模型實現,也就是說Redis使用一個線程來服務所有的客戶端請求

同時Redis採用了非阻塞式IO,並精細地優化各種命令的算法時間複雜度,這些信息意味著:

  • Redis是線程安全的(因為只有一個線程),其所有操作都是原子的,不會因併發產生數據異常
  • Redis的速度非常快(因為使用非阻塞式IO,且大部分命令的算法時間複雜度都是O(1))
  • 使用高耗時的Redis命令是很危險的,會佔用唯一的一個線程的大量處理時間,導致所有的請求都被拖慢。(例如時間複雜度為O(N)的KEYS命令,嚴格禁止在生產環境中使用)

Redis的數據結構和相關常用命令

本節將介紹Redis支持的主要數據結構,以及相關的常用Redis命令

Key

Redis採用Key-Value型的基本數據結構,任何二進制序列都可以作為Redis的Key使用(例如普通的字符串或一張JPEG圖片)

關於Key的一些注意事項:

  • 不要使用過長的Key。例如使用一個1024字節的key就不是一個好主意,不僅會消耗更多的內存,還會導致查找的效率降低
  • Key短到缺失了可讀性也是不好的,例如"u1000flw"比起"user:1000:followers"來說,節省了寥寥的存儲空間,卻引發了可讀性和可維護性上的麻煩
  • 最好使用統一的規範來設計Key,比如"object-type:id:attr",以這一規範設計出的Key可能是"user:1000"或"comment:1234:reply-to"
  • Redis允許的最大Key長度是512MB(對Value的長度限制也是512MB)

String

String是Redis的基礎數據類型,Redis沒有Int、Float、Boolean等數據類型的概念,所有的基本類型在Redis中都以String體現。

與String相關的常用命令:

  • SET:為一個key設置value,可以配合EX/PX參數指定key的有效期,通過NX/XX參數針對key是否存在的情況進行區別操作,時間複雜度O(1)
  • GET:獲取某個key對應的value,時間複雜度O(1)
  • GETSET:為一個key設置value,並返回該key的原value,時間複雜度O(1)
  • MSET:為多個key設置value,時間複雜度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一個已存在,則不進行任何操作,時間複雜度O(N)
  • MGET:獲取多個key對應的value,時間複雜度O(N)

上文提到過,Redis的基本數據類型只有String,但Redis可以把String作為整型或浮點型數字來使用,主要體現在INCR、DECR類的命令上:

  • INCR:將key對應的value值自增1,並返回自增後的值。只對可以轉換為整型的String數據起作用。時間複雜度O(1)
  • INCRBY:將key對應的value值自增指定的整型數值,並返回自增後的值。只對可以轉換為整型的String數據起作用。時間複雜度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改為自減。

INCR/DECR系列命令要求操作的value類型為String,並可以轉換為64位帶符號的整型數字,否則會返回錯誤。

也就是說,進行INCR/DECR系列命令的value,必須在[-2^63 ~ 2^63 - 1]範圍內。

前文提到過,Redis採用單線程模型,天然是線程安全的,這使得INCR/DECR命令可以非常便利的實現高併發場景下的精確控制。

例1:庫存控制

在高併發場景下實現庫存餘量的精準校驗,確保不出現超賣的情況。

設置庫存總量:

SET inv:remain "100"

庫存扣減+餘量校驗:

DECR inv:remain

當DECR命令返回值大於等於0時,說明庫存餘量校驗通過,如果返回小於0的值,則說明庫存已耗盡。

假設同時有300個併發請求進行庫存扣減,Redis能夠確保這300個請求分別得到99到-200的返回值,每個請求得到的返回值都是唯一的,絕對不會找出現兩個請求得到一樣的返回值的情況。

例2:自增序列生成

實現類似於RDBMS的Sequence功能,生成一系列唯一的序列號

設置序列起始值:

SET sequence "10000"

獲取一個序列值:

INCR sequence

直接將返回值作為序列使用即可。

獲取一批(如100個)序列值:

INCRBY sequence 100

假設返回值為N,那麼[N - 99 ~ N]的數值都是可用的序列值。

當多個客戶端同時向Redis申請自增序列時,Redis能夠確保每個客戶端得到的序列值或序列範圍都是全局唯一的,絕對不會出現不同客戶端得到了重複的序列值的情況。

List

Redis的List是鏈表型的數據結構,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的兩端執行插入元素和彈出元素的操作。

雖然List也支持在特定index上插入和讀取元素的功能,但其時間複雜度較高(O(N)),應小心使用。

與List相關的常用命令:

  • LPUSH:向指定List的左側(即頭部)插入1個或多個元素,返回插入後的List長度。時間複雜度O(N),N為插入元素的數量
  • RPUSH:同LPUSH,向指定List的右側(即尾部)插入1或多個元素
  • LPOP:從指定List的左側(即頭部)移除一個元素並返回,時間複雜度O(1)
  • RPOP:同LPOP,從指定List的右側(即尾部)移除1個元素並返回
  • LPUSHX/RPUSHX:與LPUSH/RPUSH類似,區別在於,LPUSHX/RPUSHX操作的key如果不存在,則不會進行任何操作
  • LLEN:返回指定List的長度,時間複雜度O(1)
  • LRANGE:返回指定List中指定範圍的元素(雙端包含,即LRANGE key 0 10會返回11個元素),時間複雜度O(N)。


注意:應儘可能控制一次獲取的元素數量,一次獲取過大範圍的List元素會導致延遲,同時對長度不可預知的List,避免使用LRANGE key 0 -1這樣的完整遍歷操作。

應謹慎使用的List相關命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index數值是迴環的,即-1代表List最後一個位置,-2代表List倒數第二個位置。時間複雜度O(N)
  • LSET:將指定List指定index上的元素設置為value,如果index越界則返回錯誤,時間複雜度O(N),如果操作的是頭/尾部的元素,則時間複雜度為O(1)
  • LINSERT:向指定List中指定元素之前/之後插入一個新元素,並返回操作後的List長度。如果指定的元素不存在,返回-1。如果指定key不存在,不會進行任何操作,時間複雜度O(N)

由於Redis的List是鏈表結構的,上述的三個命令的算法效率較低,需要對List進行遍歷,命令的耗時無法預估,在List長度大的情況下耗時會明顯增加,應謹慎使用。

換句話說,Redis的List實際是設計來用於實現隊列,而不是用於實現類似ArrayList這樣的列表的。

如果你不是想要實現一個雙端出入的隊列,那麼請儘量不要使用Redis的List數據結構。

為了更好支持隊列的特性,Redis還提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能夠實現類似於BlockingQueue的能力,即在List為空時,阻塞該連接,直到List中有對象可以出隊時再返回。

Hash

Hash即哈希表,Redis的Hash和傳統的哈希表一樣,是一種field-value型的數據結構,可以理解成將HashMap搬入Redis。

Hash非常適合用於表現對象類型的數據,用Hash中的field對應對象的field即可。

Hash的優點包括:

  • 可以實現二元查找,如"查找ID為1000的用戶的年齡"
  • 比起將整個對象序列化後作為String存儲的方法,Hash能夠有效地減少網絡傳輸的消耗
  • 當使用Hash維護一個集合時,提供了比List效率高得多的隨機訪問命令

與Hash相關的常用命令:

  • HSET:將key對應的Hash中的field設置為value。如果該Hash不存在,會自動創建一個。時間複雜度O(1)
  • HGET:返回指定Hash中field字段的值,時間複雜度O(1)


  • HMSET/HMGET:同HSET和HGET,可以批量操作同一個key下的多個field,時間複雜度:O(N),N為一次操作的field數量
  • HSETNX:同HSET,但如field已經存在,HSETNX不會進行任何操作,時間複雜度O(1)
  • HEXISTS:判斷指定Hash中field是否存在,存在返回1,不存在返回0,時間複雜度O(1)
  • HDEL:刪除指定Hash中的field(1個或多個),時間複雜度:O(N),N為操作的field數量
  • HINCRBY:同INCRBY命令,對指定Hash中的一個field進行INCRBY,時間複雜度O(1)

應謹慎使用的Hash相關命令:

  • HGETALL:返回指定Hash中所有的field-value對。返回結果為數組,數組中field和value交替出現。時間複雜度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,時間複雜度O(N)

上述三個命令都會對Hash進行完整遍歷,Hash中的field數量與命令的耗時線性相關

對於尺寸不可預知的Hash,應嚴格避免使用上面三個命令,而改為使用HSCAN命令進行遊標式的遍歷

Set

Redis Set是無序的,不可重複的String集合。

與Set相關的常用命令:

  • SADD:向指定Set中添加1個或多個member,如果指定Set不存在,會自動創建一個。時間複雜度O(N),N為添加的member個數
  • SREM:從指定Set中移除1個或多個member,時間複雜度O(N),N為移除的member個數
  • SRANDMEMBER:從指定Set中隨機返回1個或多個member,時間複雜度O(N),N為返回的member個數
  • SPOP:從指定Set中隨機移除並返回count個member,時間複雜度O(N),N為移除的member個數
  • SCARD:返回指定Set中的member個數,時間複雜度O(1)
  • SISMEMBER:判斷指定的value是否存在於指定Set中,時間複雜度O(1)
  • SMOVE:將指定member從一個Set移至另一個Set

慎用的Set相關命令:

  • SMEMBERS:返回指定Hash中所有的member,時間複雜度O(N)
  • SUNION/SUNIONSTORE:計算多個Set的並集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數
  • SINTER/SINTERSTORE:計算多個Set的交集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數
  • SDIFF/SDIFFSTORE:計算1個Set與1或多個Set的差集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數

上述幾個命令涉及的計算量大,應謹慎使用,特別是在參與計算的Set尺寸不可知的情況下,應嚴格避免使用。

Sorted Set

Redis Sorted Set是有序的、不可重複的String集合。

Sorted Set中的每個元素都需要指派一個分數(score),Sorted Set會根據score對元素進行升序排序。

如果多個member擁有相同的score,則以字典序進行升序排序。Sorted Set非常適合用於實現排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1個或多個member,時間複雜度O(Mlog(N)),M為添加的member數量,N為Sorted Set中的member數量
  • ZREM:從指定Sorted Set中刪除1個或多個member,時間複雜度O(Mlog(N)),M為刪除的member數量,N為Sorted Set中的member數量
  • ZCOUNT:返回指定Sorted Set中指定score範圍內的member數量,時間複雜度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member數量,時間複雜度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,時間複雜度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK則返回按降序排序的排名。時間複雜度O(log(N))
  • ZINCRBY:同INCRBY,對指定Sorted Set中的指定member的score進行自增,時間複雜度O(log(N))

慎用的Sorted Set相關命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名範圍內的所有member,ZRANGE為按score升序排序,ZREVRANGE為按score降序排序,時間複雜度O(log(N)+M),M為本次返回的member數
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score範圍內的所有member,返回結果以升序/降序排序,min和max可以指定為-inf和+inf,代表返回所有的member。時間複雜度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名範圍/指定score範圍內的所有member。時間複雜度O(log(N)+M)

上述幾個命令,應儘量避免傳遞[0 -1]或[-inf +inf]這樣的參數,來對Sorted Set做一次性的完整遍歷,特別是在Sorted Set的尺寸不可預知的情況下。

Bitmap和HyperLogLog

Redis的這兩種數據結構相較之前的並不常用,在本文中只做簡要介紹

Bitmap在Redis中不是一種實際的數據類型,而是一種將String作為Bitmap使用的方法。

可以理解為將String轉換為bit數組。使用Bitmap來存儲true/false類型的簡單數據極為節省空間。

HyperLogLogs是一種主要用於數量統計的數據結構,它和Set類似,維護一個不可重複的String集合

但是HyperLogLogs並不維護具體的member內容,只維護member的個數。

也就是說,HyperLogLogs只能用於計算一個集合中不重複的元素數量,所以它比Set要節省很多內存空間。

其他常用命令

  • EXISTS:判斷指定的key是否存在,返回1代表存在,0代表不存在,時間複雜度O(1)
  • DEL:刪除指定的key及其對應的value,時間複雜度O(N),N為刪除的key數量
  • EXPIRE/PEXPIRE:為一個key設置有效期,單位為秒或毫秒,時間複雜度O(1)
  • TTL/PTTL:返回一個key剩餘的有效時間,單位為秒或毫秒,時間複雜度O(1)
  • RENAME/RENAMENX:將key重命名為newkey。使用RENAME時,如果newkey已經存在,其值會被覆蓋;使用RENAMENX時,如果newkey已經存在,則不會進行任何操作,時間複雜度O(1)
  • TYPE:返回指定key的類型,string, list, set, zset, hash。時間複雜度O(1)
  • CONFIG GET:獲得Redis某配置項的當前值,可以使用*通配符,時間複雜度O(1)
  • CONFIG SET:為Redis某個配置項設置新值,時間複雜度O(1)
  • CONFIG REWRITE:讓Redis重新加載redis.conf中的配置

數據持久化

Redis提供了將數據定期自動持久化至硬盤的能力,包括RDB和AOF兩種方案。

兩種方案分別有其長處和短板,可以配合起來同時運行,確保數據的穩定性。

必須使用數據持久化嗎?

Redis的數據持久化機制是可以關閉的,如果你只把Redis作為緩存服務使用,Redis中存儲的所有數據都不是該數據的主體而僅僅是同步過來的備份,那麼可以關閉Redis的數據持久化機制。

但通常來說,仍然建議至少開啟RDB方式的數據持久化,因為:

  • RDB方式的持久化幾乎不損耗Redis本身的性能,在進行RDB持久化時,Redis主進程唯一需要做的事情就是fork出一個子進程,所有持久化工作都由子進程完成
  • Redis無論因為什麼原因crash掉之後,重啟時能夠自動恢復到上一次RDB快照中記錄的數據。這省去了手工從其他數據源(如DB)同步數據的過程,而且要比其他任何的數據恢復方式都要快
  • 現在硬盤那麼大,真的不缺那一點地方

RDB

採用RDB持久方式,Redis會定期保存數據快照至一個rbd文件中,並在啟動時自動加載rdb文件,恢復之前保存的數據。

可以在配置文件中配置Redis進行快照保存的時機:

save [seconds] [changes]

意為在[seconds]秒內如果發生了[changes]次數據修改,則進行一次RDB快照保存,例如:

save 60 100

會讓Redis每60秒檢查一次數據變更情況,如果發生了100次或以上的數據變更,則進行RDB快照保存。

可以配置多條save指令,讓Redis執行多級的快照保存策略。

Redis默認開啟RDB快照,默認的RDB策略如下:

save 900 1
save 300 10
save 60 10000

也可以通過BGSAVE命令手工觸發RDB快照保存。

RDB的優點:

  • 對性能影響最小。如前文所述,Redis在保存RDB快照時會fork出子進程進行,幾乎不影響Redis處理客戶端請求的效率。
  • 每次快照會生成一個完整的數據快照文件,所以可以輔以其他手段保存多個時間點的快照(例如把每天0點的快照備份至其他存儲媒介中),作為非常可靠的災難恢復手段。
  • 使用RDB文件進行數據恢復比使用AOF要快很多。

RDB的缺點:

  • 快照是定期生成的,所以在Redis crash時或多或少會丟失一部分數據。
  • 如果數據集非常大且CPU不夠強(比如單核CPU),Redis在fork子進程時可能會消耗相對較長的時間(長至1秒),影響這期間的客戶端請求。

AOF

採用AOF持久方式時,Redis會把每一個寫請求都記錄在一個日誌文件裡。

在Redis重啟時,會把AOF文件中記錄的所有寫操作順序執行一遍,確保數據恢復到最新。

AOF默認是關閉的,如要開啟,進行如下配置:

appendonly yes

AOF提供了三種fsync配置,always/everysec/no,通過配置項[appendfsync]指定:

  • appendfsync no:不進行fsync,將flush文件的時機交給OS決定,速度最快
  • appendfsync always:每寫入一條日誌就進行一次fsync操作,數據安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由後臺線程每秒fsync一次

隨著AOF不斷地記錄寫操作日誌,必定會出現一些無用的日誌

例如某個時間點執行了命令SET key1 "abc",在之後某個時間點又執行了SET key1 "bcd",那麼第一條命令很顯然是沒有用的。

大量的無用日誌會讓AOF文件過大,也會讓數據恢復的時間過長。

所以Redis提供了AOF rewrite功能,可以重寫AOF文件,只保留能夠把數據恢復到最新狀態的最小寫操作集。

AOF rewrite可以通過BGREWRITEAOF命令觸發,也可以配置Redis定期自動進行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面兩行配置的含義是,Redis在每次AOF rewrite時,會記錄完成rewrite後的AOF日誌大小,當AOF日誌大小在該基礎上增長了100%後,自動進行AOF rewrite。同時如果增長的大小沒有達到64mb,則不會進行rewrite。

AOF的優點:

  • 最安全,在啟用appendfsync always時,任何已寫入的數據都不會丟失,使用在啟用appendfsync everysec也至多隻會丟失1秒的數據。
  • AOF文件在發生斷電等問題時也不會損壞,即使出現了某條日誌只寫入了一半的情況,也可以使用redis-check-aof工具輕鬆修復。
  • AOF文件易讀,可修改,在進行了某些錯誤的數據清除操作後,只要AOF文件沒有rewrite,就可以把AOF文件備份出來,把錯誤的命令刪除,然後恢復數據。

AOF的缺點:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 數據恢復速度比RDB慢

內存管理與數據淘汰機制

最大內存設置

默認情況下,在32位OS中,Redis最大使用3GB的內存,在64位OS中則沒有限制。

在使用Redis時,應該對數據佔用的最大空間有一個基本準確的預估,併為Redis設定最大使用的內存。

否則在64位OS中Redis會無限制地佔用內存(當物理內存被佔滿後會使用swap空間),容易引發各種各樣的問題。

通過如下配置控制Redis使用的最大內存:

maxmemory 100mb

在內存佔用達到了maxmemory後,再向Redis寫入數據時,Redis會:

  • 根據配置的數據淘汰策略嘗試淘汰數據,釋放空間
  • 如果沒有數據可以淘汰,或者沒有配置數據淘汰策略,那麼Redis會對所有寫請求返回錯誤,但讀請求仍然可以正常執行

在為Redis設置maxmemory時,需要注意:


如果採用了Redis的主從同步,主節點向從節點同步數據時,會佔用掉一部分內存空間

如果maxmemory過於接近主機的可用內存,會導致數據同步時內存不足。

所以設置的maxmemory不要過於接近主機可用的內存,留出一部分預留用作主從同步。

數據淘汰機制

Redis提供了5種數據淘汰策略:

  • volatile-lru:使用LRU算法進行數據淘汰(淘汰上次使用時間最早的,且使用次數最少的key),只淘汰設定了有效期的key
  • allkeys-lru:使用LRU算法進行數據淘汰,所有的key都可以被淘汰
  • volatile-random:隨機淘汰數據,只淘汰設定了有效期的key
  • allkeys-random:隨機淘汰數據,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩餘有效期最短的key

最好為Redis指定一種有效的數據淘汰策略以配合maxmemory設置,避免在內存使用滿後發生寫入失敗的情況。

一般來說,推薦使用的策略是volatile-lru,並辨識Redis中保存的數據的重要性。

對於那些重要的,絕對不能丟棄的數據(如配置類數據等),應不設置有效期,這樣Redis就永遠不會淘汰這些數據。

對於那些相對不是那麼重要的,並且能夠熱加載的數據(比如緩存最近登錄的用戶信息,當在Redis中找不到時,程序會去DB中讀取),可以設置上有效期,這樣在內存不夠時Redis就會淘汰這部分數據。

配置方法:

maxmemory-policy volatile-lru #默認是noeviction,即不進行數據淘汰

Pipelining

Pipelining

Redis提供許多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,這些命令存在的意義是減少維護網絡連接和傳輸數據所消耗的資源和時間。

例如連續使用5次SET命令設置5個不同的key,比起使用一次MSET命令設置5個不同的key,效果是一樣的

但前者會消耗更多的RTT(Round Trip Time)時長,永遠應優先使用後者。

然而,如果客戶端要連續執行的多次操作無法通過Redis命令組合在一起,例如:

SET a "abc"
INCR b
HSET c name "hi"

此時便可以使用Redis提供的pipelining功能來實現在一次交互中執行多條命令。

使用pipelining時,只需要從客戶端一次向Redis發送多條命令(以\\r\\n)分隔,Redis就會依次執行這些命令,並且把每個命令的返回按順序組裝在一起一次返回,比如:

$ (printf "PING\\r\\nPING\\r\\nPING\\r\\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

大部分的Redis客戶端都對Pipelining提供支持,所以開發者通常並不需要自己手工拼裝命令列表。

Pipelining的侷限性

Pipelining只能用於執行連續且無相關性的命令,當某個命令的生成需要依賴於前一個命令的返回時,就無法使用Pipelining了。

通過Scripting功能,可以規避這一侷限性

事務與Scripting

Pipelining能夠讓Redis在一次交互中處理多條命令,然而在一些場景下,我們可能需要在此基礎上確保這一組命令是連續執行的。

比如獲取當前累計的PV數並將其清0

> GET vCount
12384
> SET vCount 0
OK

如果在GET和SET命令之間插進來一個INCR vCount,就會使客戶端拿到的vCount不準確。

Redis的事務可以確保複數命令執行時的原子性。

也就是說Redis能夠保證:一個事務中的一組命令是絕對連續執行的,在這些命令執行完成之前,絕對不會有來自於其他連接的其他命令插進去執行。

通過MULTI和EXEC命令來把這兩個命令加入一個事務中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令後便會開啟一個事務,這之後的所有讀寫命令都會保存在隊列中但並不執行

直到接收到EXEC命令後,Redis會把隊列中的所有命令連續順序執行,並以數組形式返回每個命令的返回結果。

可以使用DISCARD命令放棄當前的事務,將保存的命令隊列清空。

需要注意的是,Redis事務不支持回滾:如果一個事務中的命令出現了語法錯誤,大部分客戶端驅動會返回錯誤

2.6.5版本以上的Redis也會在執行EXEC時檢查隊列中的命令是否存在語法錯誤,如果存在,則會自動放棄事務並返回錯誤。

但如果一個事務中的命令有非語法類的錯誤(比如對String執行HSET操作),無論客戶端驅動還是Redis都無法在真正執行這條命令之前發現,所以事務中的所有命令仍然會被依次執行。

在這種情況下,會出現一個事務中部分命令成功部分命令失敗的情況,然而與RDBMS不同,Redis不提供事務回滾的功能,所以只能通過其他方法進行數據的回滾。

通過事務實現CAS

Redis提供了WATCH命令與事務搭配使用,實現CAS樂觀鎖的機制。

假設要實現將某個商品的狀態改為已售:

if(exec(HGET stock:1001 state) == "in stock")
exec(HSET stock:1001 state "sold");

這一偽代碼執行時,無法確保併發安全性,有可能多個客戶端都獲取到了"in stock"的狀態,導致一個庫存被售賣多次。

使用WATCH命令和事務可以解決這一問題:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
exec(MULTI);
exec(HSET stock:1001 state "sold");
exec(EXEC);
}

WATCH的機制是:在事務EXEC命令執行時,Redis會檢查被WATCH的key,只有被WATCH的key從WATCH起始時至今沒有發生過變更,EXEC才會被執行。

如果WATCH的key在WATCH命令到EXEC命令之間發生過變化,則EXEC命令會返回失敗。

Scripting

通過EVAL與EVALSHA命令,可以讓Redis執行LUA腳本。這就類似於RDBMS的存儲過程一樣,可以把客戶端與Redis之間密集的讀/寫交互放在服務端進行,避免過多的數據交互,提升性能。

Scripting功能是作為事務功能的替代者誕生的,事務提供的所有能力Scripting都可以做到。Redis官方推薦使用LUA Script來代替事務,前者的效率和便利性都超過了事務。

關於Scripting的具體使用,本文不做詳細介紹,請參考官方文檔 https://redis.io/commands/eval

Redis性能調優

儘管Redis是一個非常快速的內存數據存儲媒介,也並不代表Redis不會產生性能問題。

前文中提到過,Redis採用單線程模型,所有的命令都是由一個線程串行執行的

所以當某個命令執行耗時較長時,會拖慢其後的所有命令,這使得Redis對每個任務的執行效率更加敏感。

針對Redis的性能優化,主要從下面幾個層面入手:

  • 最初的也是最重要的,確保沒有讓Redis執行耗時長的命令
  • 使用pipelining將連續執行的命令組合執行
  • 操作系統的Transparent huge pages功能必須關閉:
echo never > /sys/kernel/mm/transparent_hugepage/enabled

  • 如果在虛擬機中運行Redis,可能天然就有虛擬機環境帶來的固有延遲。
  • 可以通過./redis-cli --intrinsic-latency 100命令查看固有延遲。同時如果對Redis的性能有較高要求的話,應儘可能在物理機上直接部署Redis。
  • 檢查數據持久化策略
  • 考慮引入讀寫分離機制
"
你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)

目錄

  • 概述
  • Redis的數據結構和常用命令
  • 數據持久化
  • 內存管理與數據淘汰機制
  • Pipelining
  • 事務與Scripting
  • Redis性能調優
  • 主從複製與集群分片
  • Redis Java客戶端的選擇


本文將從Redis的基本特性入手,通過講述Redis的數據結構和主要命令對Redis的基本能力進行直觀介紹。

然後概覽Redis提供的高級能力,並在部署、維護、性能調優等多個方面進行更深入的介紹和指導。

本文適合使用Redis的普通開發人員,以及對Redis進行選型、架構設計和性能調優的架構設計人員。

概述

Redis是一個開源的,基於內存的結構化數據存儲媒介,可以作為數據庫、緩存服務或消息服務使用。

Redis支持多種數據結構,包括字符串、哈希表、鏈表、集合、有序集合、位圖、Hyperloglogs等。

Redis具備LRU淘汰、事務實現、以及不同級別的硬盤持久化等能力,並且支持副本集和通過Redis Sentinel實現的高可用方案,同時還支持通過Redis Cluster實現的數據自動分片能力。

Redis的主要功能都基於單線程模型實現,也就是說Redis使用一個線程來服務所有的客戶端請求

同時Redis採用了非阻塞式IO,並精細地優化各種命令的算法時間複雜度,這些信息意味著:

  • Redis是線程安全的(因為只有一個線程),其所有操作都是原子的,不會因併發產生數據異常
  • Redis的速度非常快(因為使用非阻塞式IO,且大部分命令的算法時間複雜度都是O(1))
  • 使用高耗時的Redis命令是很危險的,會佔用唯一的一個線程的大量處理時間,導致所有的請求都被拖慢。(例如時間複雜度為O(N)的KEYS命令,嚴格禁止在生產環境中使用)

Redis的數據結構和相關常用命令

本節將介紹Redis支持的主要數據結構,以及相關的常用Redis命令

Key

Redis採用Key-Value型的基本數據結構,任何二進制序列都可以作為Redis的Key使用(例如普通的字符串或一張JPEG圖片)

關於Key的一些注意事項:

  • 不要使用過長的Key。例如使用一個1024字節的key就不是一個好主意,不僅會消耗更多的內存,還會導致查找的效率降低
  • Key短到缺失了可讀性也是不好的,例如"u1000flw"比起"user:1000:followers"來說,節省了寥寥的存儲空間,卻引發了可讀性和可維護性上的麻煩
  • 最好使用統一的規範來設計Key,比如"object-type:id:attr",以這一規範設計出的Key可能是"user:1000"或"comment:1234:reply-to"
  • Redis允許的最大Key長度是512MB(對Value的長度限制也是512MB)

String

String是Redis的基礎數據類型,Redis沒有Int、Float、Boolean等數據類型的概念,所有的基本類型在Redis中都以String體現。

與String相關的常用命令:

  • SET:為一個key設置value,可以配合EX/PX參數指定key的有效期,通過NX/XX參數針對key是否存在的情況進行區別操作,時間複雜度O(1)
  • GET:獲取某個key對應的value,時間複雜度O(1)
  • GETSET:為一個key設置value,並返回該key的原value,時間複雜度O(1)
  • MSET:為多個key設置value,時間複雜度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一個已存在,則不進行任何操作,時間複雜度O(N)
  • MGET:獲取多個key對應的value,時間複雜度O(N)

上文提到過,Redis的基本數據類型只有String,但Redis可以把String作為整型或浮點型數字來使用,主要體現在INCR、DECR類的命令上:

  • INCR:將key對應的value值自增1,並返回自增後的值。只對可以轉換為整型的String數據起作用。時間複雜度O(1)
  • INCRBY:將key對應的value值自增指定的整型數值,並返回自增後的值。只對可以轉換為整型的String數據起作用。時間複雜度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改為自減。

INCR/DECR系列命令要求操作的value類型為String,並可以轉換為64位帶符號的整型數字,否則會返回錯誤。

也就是說,進行INCR/DECR系列命令的value,必須在[-2^63 ~ 2^63 - 1]範圍內。

前文提到過,Redis採用單線程模型,天然是線程安全的,這使得INCR/DECR命令可以非常便利的實現高併發場景下的精確控制。

例1:庫存控制

在高併發場景下實現庫存餘量的精準校驗,確保不出現超賣的情況。

設置庫存總量:

SET inv:remain "100"

庫存扣減+餘量校驗:

DECR inv:remain

當DECR命令返回值大於等於0時,說明庫存餘量校驗通過,如果返回小於0的值,則說明庫存已耗盡。

假設同時有300個併發請求進行庫存扣減,Redis能夠確保這300個請求分別得到99到-200的返回值,每個請求得到的返回值都是唯一的,絕對不會找出現兩個請求得到一樣的返回值的情況。

例2:自增序列生成

實現類似於RDBMS的Sequence功能,生成一系列唯一的序列號

設置序列起始值:

SET sequence "10000"

獲取一個序列值:

INCR sequence

直接將返回值作為序列使用即可。

獲取一批(如100個)序列值:

INCRBY sequence 100

假設返回值為N,那麼[N - 99 ~ N]的數值都是可用的序列值。

當多個客戶端同時向Redis申請自增序列時,Redis能夠確保每個客戶端得到的序列值或序列範圍都是全局唯一的,絕對不會出現不同客戶端得到了重複的序列值的情況。

List

Redis的List是鏈表型的數據結構,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的兩端執行插入元素和彈出元素的操作。

雖然List也支持在特定index上插入和讀取元素的功能,但其時間複雜度較高(O(N)),應小心使用。

與List相關的常用命令:

  • LPUSH:向指定List的左側(即頭部)插入1個或多個元素,返回插入後的List長度。時間複雜度O(N),N為插入元素的數量
  • RPUSH:同LPUSH,向指定List的右側(即尾部)插入1或多個元素
  • LPOP:從指定List的左側(即頭部)移除一個元素並返回,時間複雜度O(1)
  • RPOP:同LPOP,從指定List的右側(即尾部)移除1個元素並返回
  • LPUSHX/RPUSHX:與LPUSH/RPUSH類似,區別在於,LPUSHX/RPUSHX操作的key如果不存在,則不會進行任何操作
  • LLEN:返回指定List的長度,時間複雜度O(1)
  • LRANGE:返回指定List中指定範圍的元素(雙端包含,即LRANGE key 0 10會返回11個元素),時間複雜度O(N)。


注意:應儘可能控制一次獲取的元素數量,一次獲取過大範圍的List元素會導致延遲,同時對長度不可預知的List,避免使用LRANGE key 0 -1這樣的完整遍歷操作。

應謹慎使用的List相關命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index數值是迴環的,即-1代表List最後一個位置,-2代表List倒數第二個位置。時間複雜度O(N)
  • LSET:將指定List指定index上的元素設置為value,如果index越界則返回錯誤,時間複雜度O(N),如果操作的是頭/尾部的元素,則時間複雜度為O(1)
  • LINSERT:向指定List中指定元素之前/之後插入一個新元素,並返回操作後的List長度。如果指定的元素不存在,返回-1。如果指定key不存在,不會進行任何操作,時間複雜度O(N)

由於Redis的List是鏈表結構的,上述的三個命令的算法效率較低,需要對List進行遍歷,命令的耗時無法預估,在List長度大的情況下耗時會明顯增加,應謹慎使用。

換句話說,Redis的List實際是設計來用於實現隊列,而不是用於實現類似ArrayList這樣的列表的。

如果你不是想要實現一個雙端出入的隊列,那麼請儘量不要使用Redis的List數據結構。

為了更好支持隊列的特性,Redis還提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能夠實現類似於BlockingQueue的能力,即在List為空時,阻塞該連接,直到List中有對象可以出隊時再返回。

Hash

Hash即哈希表,Redis的Hash和傳統的哈希表一樣,是一種field-value型的數據結構,可以理解成將HashMap搬入Redis。

Hash非常適合用於表現對象類型的數據,用Hash中的field對應對象的field即可。

Hash的優點包括:

  • 可以實現二元查找,如"查找ID為1000的用戶的年齡"
  • 比起將整個對象序列化後作為String存儲的方法,Hash能夠有效地減少網絡傳輸的消耗
  • 當使用Hash維護一個集合時,提供了比List效率高得多的隨機訪問命令

與Hash相關的常用命令:

  • HSET:將key對應的Hash中的field設置為value。如果該Hash不存在,會自動創建一個。時間複雜度O(1)
  • HGET:返回指定Hash中field字段的值,時間複雜度O(1)


  • HMSET/HMGET:同HSET和HGET,可以批量操作同一個key下的多個field,時間複雜度:O(N),N為一次操作的field數量
  • HSETNX:同HSET,但如field已經存在,HSETNX不會進行任何操作,時間複雜度O(1)
  • HEXISTS:判斷指定Hash中field是否存在,存在返回1,不存在返回0,時間複雜度O(1)
  • HDEL:刪除指定Hash中的field(1個或多個),時間複雜度:O(N),N為操作的field數量
  • HINCRBY:同INCRBY命令,對指定Hash中的一個field進行INCRBY,時間複雜度O(1)

應謹慎使用的Hash相關命令:

  • HGETALL:返回指定Hash中所有的field-value對。返回結果為數組,數組中field和value交替出現。時間複雜度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,時間複雜度O(N)

上述三個命令都會對Hash進行完整遍歷,Hash中的field數量與命令的耗時線性相關

對於尺寸不可預知的Hash,應嚴格避免使用上面三個命令,而改為使用HSCAN命令進行遊標式的遍歷

Set

Redis Set是無序的,不可重複的String集合。

與Set相關的常用命令:

  • SADD:向指定Set中添加1個或多個member,如果指定Set不存在,會自動創建一個。時間複雜度O(N),N為添加的member個數
  • SREM:從指定Set中移除1個或多個member,時間複雜度O(N),N為移除的member個數
  • SRANDMEMBER:從指定Set中隨機返回1個或多個member,時間複雜度O(N),N為返回的member個數
  • SPOP:從指定Set中隨機移除並返回count個member,時間複雜度O(N),N為移除的member個數
  • SCARD:返回指定Set中的member個數,時間複雜度O(1)
  • SISMEMBER:判斷指定的value是否存在於指定Set中,時間複雜度O(1)
  • SMOVE:將指定member從一個Set移至另一個Set

慎用的Set相關命令:

  • SMEMBERS:返回指定Hash中所有的member,時間複雜度O(N)
  • SUNION/SUNIONSTORE:計算多個Set的並集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數
  • SINTER/SINTERSTORE:計算多個Set的交集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數
  • SDIFF/SDIFFSTORE:計算1個Set與1或多個Set的差集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數

上述幾個命令涉及的計算量大,應謹慎使用,特別是在參與計算的Set尺寸不可知的情況下,應嚴格避免使用。

Sorted Set

Redis Sorted Set是有序的、不可重複的String集合。

Sorted Set中的每個元素都需要指派一個分數(score),Sorted Set會根據score對元素進行升序排序。

如果多個member擁有相同的score,則以字典序進行升序排序。Sorted Set非常適合用於實現排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1個或多個member,時間複雜度O(Mlog(N)),M為添加的member數量,N為Sorted Set中的member數量
  • ZREM:從指定Sorted Set中刪除1個或多個member,時間複雜度O(Mlog(N)),M為刪除的member數量,N為Sorted Set中的member數量
  • ZCOUNT:返回指定Sorted Set中指定score範圍內的member數量,時間複雜度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member數量,時間複雜度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,時間複雜度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK則返回按降序排序的排名。時間複雜度O(log(N))
  • ZINCRBY:同INCRBY,對指定Sorted Set中的指定member的score進行自增,時間複雜度O(log(N))

慎用的Sorted Set相關命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名範圍內的所有member,ZRANGE為按score升序排序,ZREVRANGE為按score降序排序,時間複雜度O(log(N)+M),M為本次返回的member數
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score範圍內的所有member,返回結果以升序/降序排序,min和max可以指定為-inf和+inf,代表返回所有的member。時間複雜度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名範圍/指定score範圍內的所有member。時間複雜度O(log(N)+M)

上述幾個命令,應儘量避免傳遞[0 -1]或[-inf +inf]這樣的參數,來對Sorted Set做一次性的完整遍歷,特別是在Sorted Set的尺寸不可預知的情況下。

Bitmap和HyperLogLog

Redis的這兩種數據結構相較之前的並不常用,在本文中只做簡要介紹

Bitmap在Redis中不是一種實際的數據類型,而是一種將String作為Bitmap使用的方法。

可以理解為將String轉換為bit數組。使用Bitmap來存儲true/false類型的簡單數據極為節省空間。

HyperLogLogs是一種主要用於數量統計的數據結構,它和Set類似,維護一個不可重複的String集合

但是HyperLogLogs並不維護具體的member內容,只維護member的個數。

也就是說,HyperLogLogs只能用於計算一個集合中不重複的元素數量,所以它比Set要節省很多內存空間。

其他常用命令

  • EXISTS:判斷指定的key是否存在,返回1代表存在,0代表不存在,時間複雜度O(1)
  • DEL:刪除指定的key及其對應的value,時間複雜度O(N),N為刪除的key數量
  • EXPIRE/PEXPIRE:為一個key設置有效期,單位為秒或毫秒,時間複雜度O(1)
  • TTL/PTTL:返回一個key剩餘的有效時間,單位為秒或毫秒,時間複雜度O(1)
  • RENAME/RENAMENX:將key重命名為newkey。使用RENAME時,如果newkey已經存在,其值會被覆蓋;使用RENAMENX時,如果newkey已經存在,則不會進行任何操作,時間複雜度O(1)
  • TYPE:返回指定key的類型,string, list, set, zset, hash。時間複雜度O(1)
  • CONFIG GET:獲得Redis某配置項的當前值,可以使用*通配符,時間複雜度O(1)
  • CONFIG SET:為Redis某個配置項設置新值,時間複雜度O(1)
  • CONFIG REWRITE:讓Redis重新加載redis.conf中的配置

數據持久化

Redis提供了將數據定期自動持久化至硬盤的能力,包括RDB和AOF兩種方案。

兩種方案分別有其長處和短板,可以配合起來同時運行,確保數據的穩定性。

必須使用數據持久化嗎?

Redis的數據持久化機制是可以關閉的,如果你只把Redis作為緩存服務使用,Redis中存儲的所有數據都不是該數據的主體而僅僅是同步過來的備份,那麼可以關閉Redis的數據持久化機制。

但通常來說,仍然建議至少開啟RDB方式的數據持久化,因為:

  • RDB方式的持久化幾乎不損耗Redis本身的性能,在進行RDB持久化時,Redis主進程唯一需要做的事情就是fork出一個子進程,所有持久化工作都由子進程完成
  • Redis無論因為什麼原因crash掉之後,重啟時能夠自動恢復到上一次RDB快照中記錄的數據。這省去了手工從其他數據源(如DB)同步數據的過程,而且要比其他任何的數據恢復方式都要快
  • 現在硬盤那麼大,真的不缺那一點地方

RDB

採用RDB持久方式,Redis會定期保存數據快照至一個rbd文件中,並在啟動時自動加載rdb文件,恢復之前保存的數據。

可以在配置文件中配置Redis進行快照保存的時機:

save [seconds] [changes]

意為在[seconds]秒內如果發生了[changes]次數據修改,則進行一次RDB快照保存,例如:

save 60 100

會讓Redis每60秒檢查一次數據變更情況,如果發生了100次或以上的數據變更,則進行RDB快照保存。

可以配置多條save指令,讓Redis執行多級的快照保存策略。

Redis默認開啟RDB快照,默認的RDB策略如下:

save 900 1
save 300 10
save 60 10000

也可以通過BGSAVE命令手工觸發RDB快照保存。

RDB的優點:

  • 對性能影響最小。如前文所述,Redis在保存RDB快照時會fork出子進程進行,幾乎不影響Redis處理客戶端請求的效率。
  • 每次快照會生成一個完整的數據快照文件,所以可以輔以其他手段保存多個時間點的快照(例如把每天0點的快照備份至其他存儲媒介中),作為非常可靠的災難恢復手段。
  • 使用RDB文件進行數據恢復比使用AOF要快很多。

RDB的缺點:

  • 快照是定期生成的,所以在Redis crash時或多或少會丟失一部分數據。
  • 如果數據集非常大且CPU不夠強(比如單核CPU),Redis在fork子進程時可能會消耗相對較長的時間(長至1秒),影響這期間的客戶端請求。

AOF

採用AOF持久方式時,Redis會把每一個寫請求都記錄在一個日誌文件裡。

在Redis重啟時,會把AOF文件中記錄的所有寫操作順序執行一遍,確保數據恢復到最新。

AOF默認是關閉的,如要開啟,進行如下配置:

appendonly yes

AOF提供了三種fsync配置,always/everysec/no,通過配置項[appendfsync]指定:

  • appendfsync no:不進行fsync,將flush文件的時機交給OS決定,速度最快
  • appendfsync always:每寫入一條日誌就進行一次fsync操作,數據安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由後臺線程每秒fsync一次

隨著AOF不斷地記錄寫操作日誌,必定會出現一些無用的日誌

例如某個時間點執行了命令SET key1 "abc",在之後某個時間點又執行了SET key1 "bcd",那麼第一條命令很顯然是沒有用的。

大量的無用日誌會讓AOF文件過大,也會讓數據恢復的時間過長。

所以Redis提供了AOF rewrite功能,可以重寫AOF文件,只保留能夠把數據恢復到最新狀態的最小寫操作集。

AOF rewrite可以通過BGREWRITEAOF命令觸發,也可以配置Redis定期自動進行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面兩行配置的含義是,Redis在每次AOF rewrite時,會記錄完成rewrite後的AOF日誌大小,當AOF日誌大小在該基礎上增長了100%後,自動進行AOF rewrite。同時如果增長的大小沒有達到64mb,則不會進行rewrite。

AOF的優點:

  • 最安全,在啟用appendfsync always時,任何已寫入的數據都不會丟失,使用在啟用appendfsync everysec也至多隻會丟失1秒的數據。
  • AOF文件在發生斷電等問題時也不會損壞,即使出現了某條日誌只寫入了一半的情況,也可以使用redis-check-aof工具輕鬆修復。
  • AOF文件易讀,可修改,在進行了某些錯誤的數據清除操作後,只要AOF文件沒有rewrite,就可以把AOF文件備份出來,把錯誤的命令刪除,然後恢復數據。

AOF的缺點:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 數據恢復速度比RDB慢

內存管理與數據淘汰機制

最大內存設置

默認情況下,在32位OS中,Redis最大使用3GB的內存,在64位OS中則沒有限制。

在使用Redis時,應該對數據佔用的最大空間有一個基本準確的預估,併為Redis設定最大使用的內存。

否則在64位OS中Redis會無限制地佔用內存(當物理內存被佔滿後會使用swap空間),容易引發各種各樣的問題。

通過如下配置控制Redis使用的最大內存:

maxmemory 100mb

在內存佔用達到了maxmemory後,再向Redis寫入數據時,Redis會:

  • 根據配置的數據淘汰策略嘗試淘汰數據,釋放空間
  • 如果沒有數據可以淘汰,或者沒有配置數據淘汰策略,那麼Redis會對所有寫請求返回錯誤,但讀請求仍然可以正常執行

在為Redis設置maxmemory時,需要注意:


如果採用了Redis的主從同步,主節點向從節點同步數據時,會佔用掉一部分內存空間

如果maxmemory過於接近主機的可用內存,會導致數據同步時內存不足。

所以設置的maxmemory不要過於接近主機可用的內存,留出一部分預留用作主從同步。

數據淘汰機制

Redis提供了5種數據淘汰策略:

  • volatile-lru:使用LRU算法進行數據淘汰(淘汰上次使用時間最早的,且使用次數最少的key),只淘汰設定了有效期的key
  • allkeys-lru:使用LRU算法進行數據淘汰,所有的key都可以被淘汰
  • volatile-random:隨機淘汰數據,只淘汰設定了有效期的key
  • allkeys-random:隨機淘汰數據,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩餘有效期最短的key

最好為Redis指定一種有效的數據淘汰策略以配合maxmemory設置,避免在內存使用滿後發生寫入失敗的情況。

一般來說,推薦使用的策略是volatile-lru,並辨識Redis中保存的數據的重要性。

對於那些重要的,絕對不能丟棄的數據(如配置類數據等),應不設置有效期,這樣Redis就永遠不會淘汰這些數據。

對於那些相對不是那麼重要的,並且能夠熱加載的數據(比如緩存最近登錄的用戶信息,當在Redis中找不到時,程序會去DB中讀取),可以設置上有效期,這樣在內存不夠時Redis就會淘汰這部分數據。

配置方法:

maxmemory-policy volatile-lru #默認是noeviction,即不進行數據淘汰

Pipelining

Pipelining

Redis提供許多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,這些命令存在的意義是減少維護網絡連接和傳輸數據所消耗的資源和時間。

例如連續使用5次SET命令設置5個不同的key,比起使用一次MSET命令設置5個不同的key,效果是一樣的

但前者會消耗更多的RTT(Round Trip Time)時長,永遠應優先使用後者。

然而,如果客戶端要連續執行的多次操作無法通過Redis命令組合在一起,例如:

SET a "abc"
INCR b
HSET c name "hi"

此時便可以使用Redis提供的pipelining功能來實現在一次交互中執行多條命令。

使用pipelining時,只需要從客戶端一次向Redis發送多條命令(以\\r\\n)分隔,Redis就會依次執行這些命令,並且把每個命令的返回按順序組裝在一起一次返回,比如:

$ (printf "PING\\r\\nPING\\r\\nPING\\r\\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

大部分的Redis客戶端都對Pipelining提供支持,所以開發者通常並不需要自己手工拼裝命令列表。

Pipelining的侷限性

Pipelining只能用於執行連續且無相關性的命令,當某個命令的生成需要依賴於前一個命令的返回時,就無法使用Pipelining了。

通過Scripting功能,可以規避這一侷限性

事務與Scripting

Pipelining能夠讓Redis在一次交互中處理多條命令,然而在一些場景下,我們可能需要在此基礎上確保這一組命令是連續執行的。

比如獲取當前累計的PV數並將其清0

> GET vCount
12384
> SET vCount 0
OK

如果在GET和SET命令之間插進來一個INCR vCount,就會使客戶端拿到的vCount不準確。

Redis的事務可以確保複數命令執行時的原子性。

也就是說Redis能夠保證:一個事務中的一組命令是絕對連續執行的,在這些命令執行完成之前,絕對不會有來自於其他連接的其他命令插進去執行。

通過MULTI和EXEC命令來把這兩個命令加入一個事務中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令後便會開啟一個事務,這之後的所有讀寫命令都會保存在隊列中但並不執行

直到接收到EXEC命令後,Redis會把隊列中的所有命令連續順序執行,並以數組形式返回每個命令的返回結果。

可以使用DISCARD命令放棄當前的事務,將保存的命令隊列清空。

需要注意的是,Redis事務不支持回滾:如果一個事務中的命令出現了語法錯誤,大部分客戶端驅動會返回錯誤

2.6.5版本以上的Redis也會在執行EXEC時檢查隊列中的命令是否存在語法錯誤,如果存在,則會自動放棄事務並返回錯誤。

但如果一個事務中的命令有非語法類的錯誤(比如對String執行HSET操作),無論客戶端驅動還是Redis都無法在真正執行這條命令之前發現,所以事務中的所有命令仍然會被依次執行。

在這種情況下,會出現一個事務中部分命令成功部分命令失敗的情況,然而與RDBMS不同,Redis不提供事務回滾的功能,所以只能通過其他方法進行數據的回滾。

通過事務實現CAS

Redis提供了WATCH命令與事務搭配使用,實現CAS樂觀鎖的機制。

假設要實現將某個商品的狀態改為已售:

if(exec(HGET stock:1001 state) == "in stock")
exec(HSET stock:1001 state "sold");

這一偽代碼執行時,無法確保併發安全性,有可能多個客戶端都獲取到了"in stock"的狀態,導致一個庫存被售賣多次。

使用WATCH命令和事務可以解決這一問題:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
exec(MULTI);
exec(HSET stock:1001 state "sold");
exec(EXEC);
}

WATCH的機制是:在事務EXEC命令執行時,Redis會檢查被WATCH的key,只有被WATCH的key從WATCH起始時至今沒有發生過變更,EXEC才會被執行。

如果WATCH的key在WATCH命令到EXEC命令之間發生過變化,則EXEC命令會返回失敗。

Scripting

通過EVAL與EVALSHA命令,可以讓Redis執行LUA腳本。這就類似於RDBMS的存儲過程一樣,可以把客戶端與Redis之間密集的讀/寫交互放在服務端進行,避免過多的數據交互,提升性能。

Scripting功能是作為事務功能的替代者誕生的,事務提供的所有能力Scripting都可以做到。Redis官方推薦使用LUA Script來代替事務,前者的效率和便利性都超過了事務。

關於Scripting的具體使用,本文不做詳細介紹,請參考官方文檔 https://redis.io/commands/eval

Redis性能調優

儘管Redis是一個非常快速的內存數據存儲媒介,也並不代表Redis不會產生性能問題。

前文中提到過,Redis採用單線程模型,所有的命令都是由一個線程串行執行的

所以當某個命令執行耗時較長時,會拖慢其後的所有命令,這使得Redis對每個任務的執行效率更加敏感。

針對Redis的性能優化,主要從下面幾個層面入手:

  • 最初的也是最重要的,確保沒有讓Redis執行耗時長的命令
  • 使用pipelining將連續執行的命令組合執行
  • 操作系統的Transparent huge pages功能必須關閉:
echo never > /sys/kernel/mm/transparent_hugepage/enabled

  • 如果在虛擬機中運行Redis,可能天然就有虛擬機環境帶來的固有延遲。
  • 可以通過./redis-cli --intrinsic-latency 100命令查看固有延遲。同時如果對Redis的性能有較高要求的話,應儘可能在物理機上直接部署Redis。
  • 檢查數據持久化策略
  • 考慮引入讀寫分離機制
你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)

長耗時命令

Redis絕大多數讀寫命令的時間複雜度都在O(1)到O(N)之間,在本文和官方文檔中均對每個命令的時間複雜度有說明。

通常來說,O(1)的命令是安全的,O(N)命令在使用時需要注意,如果N的數量級不可預知,則應避免使用。

例如對一個field數未知的Hash數據執行HGETALL/HKEYS/HVALS命令,通常來說這些命令執行的很快,但如果這個Hash中的field數量極多,耗時就會成倍增長。

又如使用SUNION對兩個Set執行Union操作,或使用SORT對List/Set執行排序操作等時,都應該嚴加註意。

避免在使用這些O(N)命令時發生問題主要有幾個辦法:

  • 不要把List當做列表使用,僅當做隊列來使用
  • 通過機制嚴格控制Hash、Set、Sorted Set的大小
  • 可能的話,將排序、並集、交集等操作放在客戶端執行
  • 絕對禁止使用KEYS命令
  • 避免一次性遍歷集合類型的所有成員,而應使用SCAN類的命令進行分批的,遊標式的遍歷


Redis提供了SCAN命令,可以對Redis中存儲的所有key進行遊標式的遍歷,避免使用KEYS命令帶來的性能問題。

同時還有SSCAN/HSCAN/ZSCAN等命令,分別用於對Set/Hash/Sorted Set中的元素進行遊標式遍歷。

SCAN類命令的使用請參考官方文檔:

https://redis.io/commands/scan

Redis提供了Slow Log功能,可以自動記錄耗時較長的命令。相關的配置參數有兩個:

slowlog-log-slower-than xxxms #執行時間慢於xxx毫秒的命令計入Slow Log
slowlog-max-len xxx #Slow Log的長度,即最大紀錄多少條Slow Log

使用SLOWLOG GET [number]命令,可以輸出最近進入Slow Log的number條命令。

使用SLOWLOG RESET命令,可以重置Slow Log

網絡引發的延遲

  • 儘可能使用長連接或連接池,避免頻繁創建銷燬連接
  • 客戶端進行的批量數據操作,應使用Pipeline特性在一次交互中完成。具體請參照本文的Pipelining章節

數據持久化引發的延遲

Redis的數據持久化工作本身就會帶來延遲,需要根據數據的安全級別和性能要求制定合理的持久化策略:

  • AOF + fsync always的設置雖然能夠絕對確保數據安全,但每個操作都會觸發一次fsync,會對Redis的性能有比較明顯的影響。
  • AOF + fsync every second是比較好的折中方案,每秒fsync一次
  • AOF + fsync never會提供AOF持久化方案下的最優性能
  • 使用RDB持久化通常會提供比使用AOF更高的性能,但需要注意RDB的策略配置
  • 每一次RDB快照和AOF Rewrite都需要Redis主進程進行fork操作。fork操作本身可能會產生較高的耗時,與CPU和Redis佔用的內存大小有關。根據具體的情況合理配置RDB快照和AOF Rewrite時機,避免過於頻繁的fork帶來的延遲

Redis在fork子進程時需要將內存分頁表拷貝至子進程以佔用了24GB內存的Redis實例為例,共需要拷貝24GB / 4kB * 8 = 48MB的數據。在使用單Xeon 2.27Ghz的物理機上,這一fork操作耗時216ms。可以通過INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗時(微秒)

Swap引發的延遲

當Linux將Redis所用的內存分頁移至swap空間時,將會阻塞Redis進程,導致Redis出現不正常的延遲。

Swap通常在物理內存不足或一些進程在進行大量I/O操作時發生,應儘可能避免上述兩種情況的出現。

/proc//smaps文件中會保存進程的swap記錄,通過查看這個文件,能夠判斷Redis的延遲是否由Swap產生。

如果這個文件中記錄了較大的Swap size,則說明延遲很有可能是Swap造成的。

數據淘汰引發的延遲

當同一秒內有大量key過期時,也會引發Redis的延遲。在使用時應儘量將key的失效時間錯開。

引入讀寫分離機制

Redis的主從複製能力可以實現一主多從的多節點架構,在這一架構下,主節點接收所有寫請求,並將數據同步給多個從節點。

在這一基礎上,我們可以讓從節點提供對實時性要求不高的讀請求服務,以減小主節點的壓力。

尤其是針對一些使用了長耗時命令的統計類任務,完全可以指定在一個或多個從節點上執行,避免這些長耗時命令影響其他請求的響應。

主從複製與集群分片

主從複製

Redis支持一主多從的主從複製架構。一個Master實例負責處理所有的寫請求,Master將寫操作同步至所有Slave。

藉助Redis的主從複製,可以實現讀寫分離和高可用:

  • 實時性要求不是特別高的讀請求,可以在Slave上完成,提升效率。特別是一些週期性執行的統計任務,這些任務可能需要執行一些長耗時的Redis命令,可以專門規劃出1個或幾個Slave用於服務這些統計任務
  • 藉助Redis Sentinel可以實現高可用,當Master crash後,Redis Sentinel能夠自動將一個Slave晉升為Master,繼續提供服務

啟用主從複製非常簡單,只需要配置多個Redis實例,在作為Slave的Redis實例中配置:

slaveof 192.168.1.1 6379 #指定Master的IP和端口

當Slave啟動後,會從Master進行一次冷啟動數據同步

由Master觸發BGSAVE生成RDB文件推送給Slave進行導入,導入完成後Master再將增量數據通過Redis Protocol同步給Slave

之後主從之間的數據便一直以Redis Protocol進行同步

使用Sentinel做自動failover

Redis的主從複製功能本身只是做數據同步,並不提供監控和自動failover能力

要通過主從複製功能來實現Redis的高可用,還需要引入一個組件:Redis Sentinel

Redis Sentinel是Redis官方開發的監控組件,可以監控Redis實例的狀態,通過Master節點自動發現Slave節點,並在監測到Master節點失效時選舉出一個新的Master,並向所有Redis實例推送新的主從配置。

Redis Sentinel需要至少部署3個實例才能形成選舉關係。

關鍵配置:

sentinel monitor mymaster 127.0.0.1 6379 2 #Master實例的IP、端口,以及選舉需要的贊成票數
sentinel down-after-milliseconds mymaster 60000 #多長時間沒有響應視為Master失效
sentinel failover-timeout mymaster 180000 #兩次failover嘗試間的間隔時長
sentinel parallel-syncs mymaster 1 #如果有多個Slave,可以通過此配置指定同時從新Master進行數據同步的Slave數,避免所有Slave同時進行數據同步導致查詢服務也不可用

另外需要注意的是,Redis Sentinel實現的自動failover不是在同一個IP和端口上完成的

也就是說自動failover產生的新Master提供服務的IP和端口與之前的Master是不一樣的,所以要實現HA,還要求客戶端必須支持Sentinel,能夠與Sentinel交互獲得新Master的信息才行。

集群分片

為何要做集群分片:

  • Redis中存儲的數據量大,一臺主機的物理內存已經無法容納
  • Redis的寫請求併發量大,一個Redis實例以無法承載

當上述兩個問題出現時,就必須要對Redis進行分片了。

Redis的分片方案有很多種,例如很多Redis的客戶端都自行實現了分片功能,也有像Twemproxy這樣的以代理方式實現的Redis分片方案。

然而首選的方案還應該是Redis官方在3.0版本中推出的Redis Cluster分片方案。

本文不會對Redis Cluster的具體安裝和部署細節進行介紹,重點介紹Redis Cluster帶來的好處與弊端。

Redis Cluster的能力

  • 能夠自動將數據分散在多個節點上
  • 當訪問的key不在當前分片上時,能夠自動將請求轉發至正確的分片
  • 當集群中部分節點失效時仍能提供服務

其中第三點是基於主從複製來實現的,Redis Cluster的每個數據分片都採用了主從複製的結構,原理和前文所述的主從複製完全一致

唯一的區別是省去了Redis Sentinel這一額外的組件,由Redis Cluster負責進行一個分片內部的節點監控和自動failover。

Redis Cluster分片原理

Redis Cluster中共有16384個hash slot,Redis會計算每個key的CRC16,將結果與16384取模,來決定該key存儲在哪一個hash slot中,

同時需要指定Redis Cluster中每個數據分片負責的Slot數,Slot的分配在任何時間點都可以進行重新分配。

客戶端在對key進行讀寫操作時,可以連接Cluster中的任意一個分片,如果操作的key不在此分片負責的Slot範圍內,Redis Cluster會自動將請求重定向到正確的分片上。

hash tags

在基礎的分片原則上,Redis還支持hash tags功能,以hash tags要求的格式明明的key,將會確保進入同一個Slot中。

例如:{uiv}user:1000和{uiv}user:1001擁有同樣的hash tag {uiv},會保存在同一個Slot中。

使用Redis Cluster時,pipelining、事務和LUA Script功能涉及的key必須在同一個數據分片上,否則將會返回錯誤。

如要在Redis Cluster中使用上述功能,就必須通過hash tags來確保一個pipeline或一個事務中操作的所有key都位於同一個Slot中。

有一些客戶端(如Redisson)實現了集群化的pipelining操作,可以自動將一個pipeline裡的命令按key所在的分片進行分組,分別發到不同的分片上執行。但是Redis不支持跨分片的事務,事務和LUA Script還是必須遵循所有key在一個分片上的規則要求。

主從複製 vs 集群分片

在設計軟件架構時,要如何在主從複製和集群分片兩種部署方案中取捨呢?

從各個方面看,Redis Cluster都是優於主從複製的方案

  • Redis Cluster能夠解決單節點上數據量過大的問題
  • Redis Cluster能夠解決單節點訪問壓力過大的問題
  • Redis Cluster包含了主從複製的能力


那是不是代表Redis Cluster永遠是優於主從複製的選擇呢?

並不是!!!

軟件架構永遠不是越複雜越好,複雜的架構在帶來顯著好處的同時,一定也會帶來相應的弊端。採用Redis Cluster的弊端包括:

1、維護難度增加

在使用Redis Cluster時,需要維護的Redis實例數倍增,需要監控的主機數量也相應增加,數據備份/持久化的複雜度也會增加。

同時在進行分片的增減操作時,還需要進行reshard操作,遠比主從模式下增加一個Slave的複雜度要高。

2、客戶端資源消耗增加

當客戶端使用連接池時,需要為每一個數據分片維護一個連接池,客戶端同時需要保持的連接數成倍增多,加大了客戶端本身和操作系統資源的消耗。

3、性能優化難度增加

你可能需要在多個分片上查看Slow Log和Swap日誌才能定位性能問題。

4、事務和LUA Script的使用成本增加

在Redis Cluster中使用事務和LUA Script特性有嚴格的限制條件

事務和Script中操作的key必須位於同一個分片上,這就使得在開發時必須對相應場景下涉及的key進行額外的規劃和規範要求。

如果應用的場景中大量涉及事務和Script的使用,如何在保證這兩個功能的正常運作前提下把數據平均分到多個數據分片中就會成為難點。

所以,在主從複製和集群分片兩個方案中做出選擇時,應該從應用軟件的功能特性、數據和訪問量級、未來發展規劃等方面綜合考慮,只在確實有必要引入數據分片時再使用Redis Cluster。

下面是一些建議:

  • 需要在Redis中存儲的數據有多大?未來2年內可能發展為多大?這些數據是否都需要長期保存?是否可以使用LRU算法進行非熱點數據的淘汰?綜合考慮前面幾個因素,評估出Redis需要使用的物理內存。
  • 用於部署Redis的主機物理內存有多大?有多少可以分配給Redis使用?對比(1)中的內存需求評估,是否足夠用?
  • Redis面臨的併發寫壓力會有多大?在不使用pipelining時,Redis的寫性能可以超過10萬次/秒(更多的benchmark可以參考 https://redis.io/topics/benchmarks )
  • 在使用Redis時,是否會使用到pipelining和事務功能?使用的場景多不多?

綜合上面幾點考慮,如果單臺主機的可用物理內存完全足以支撐對Redis的容量需求,且Redis面臨的併發寫壓力距離Benchmark值還尚有距離,建議採用主從複製的架構,可以省去很多不必要的麻煩。

同時,如果應用中大量使用pipelining和事務,也建議儘可能選擇主從複製架構,可以減少設計和開發時的複雜度。

Redis Java客戶端的選擇

Redis的Java客戶端很多,官方推薦的有三種:Jedis、Redisson和lettuce。

在這裡對Jedis和Redisson進行對比介紹

Jedis:

  • 輕量,簡潔,便於集成和改造
  • 支持連接池
  • 支持pipelining、事務、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持讀寫分離,需要自己實現
  • 文檔差(真的很差,幾乎沒有……)

Redisson:

  • 基於Netty實現,採用非阻塞IO,性能高
  • 支持異步請求
  • 支持連接池
  • 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持事務,官方建議以LUA Scripting代替事務
  • 支持在Redis Cluster架構下使用pipelining
  • 支持讀寫分離,支持讀負載均衡,在主從複製和Redis Cluster架構下都可以使用
  • 內建Tomcat Session Manager,為Tomcat 6/7/8提供了會話共享功能
  • 可以與Spring Session集成,實現基於Redis的會話共享
  • 文檔較豐富,有中文文檔

對於Jedis和Redisson的選擇,同樣應遵循前述的原理,儘管Jedis比起Redisson有各種各樣的不足,但也應該在需要使用Redisson的高級特性時再選用Redisson,避免造成不必要的程序複雜度提升。

其實很多技術不是一兩篇文章就能說的清楚的。

那如何快速有效的學習並精通呢?

轉發!

轉發!

轉發後關注作者,私信關鍵詞:“ Java ”

以下價值1990元資料,免費領取!

"
你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)

目錄

  • 概述
  • Redis的數據結構和常用命令
  • 數據持久化
  • 內存管理與數據淘汰機制
  • Pipelining
  • 事務與Scripting
  • Redis性能調優
  • 主從複製與集群分片
  • Redis Java客戶端的選擇


本文將從Redis的基本特性入手,通過講述Redis的數據結構和主要命令對Redis的基本能力進行直觀介紹。

然後概覽Redis提供的高級能力,並在部署、維護、性能調優等多個方面進行更深入的介紹和指導。

本文適合使用Redis的普通開發人員,以及對Redis進行選型、架構設計和性能調優的架構設計人員。

概述

Redis是一個開源的,基於內存的結構化數據存儲媒介,可以作為數據庫、緩存服務或消息服務使用。

Redis支持多種數據結構,包括字符串、哈希表、鏈表、集合、有序集合、位圖、Hyperloglogs等。

Redis具備LRU淘汰、事務實現、以及不同級別的硬盤持久化等能力,並且支持副本集和通過Redis Sentinel實現的高可用方案,同時還支持通過Redis Cluster實現的數據自動分片能力。

Redis的主要功能都基於單線程模型實現,也就是說Redis使用一個線程來服務所有的客戶端請求

同時Redis採用了非阻塞式IO,並精細地優化各種命令的算法時間複雜度,這些信息意味著:

  • Redis是線程安全的(因為只有一個線程),其所有操作都是原子的,不會因併發產生數據異常
  • Redis的速度非常快(因為使用非阻塞式IO,且大部分命令的算法時間複雜度都是O(1))
  • 使用高耗時的Redis命令是很危險的,會佔用唯一的一個線程的大量處理時間,導致所有的請求都被拖慢。(例如時間複雜度為O(N)的KEYS命令,嚴格禁止在生產環境中使用)

Redis的數據結構和相關常用命令

本節將介紹Redis支持的主要數據結構,以及相關的常用Redis命令

Key

Redis採用Key-Value型的基本數據結構,任何二進制序列都可以作為Redis的Key使用(例如普通的字符串或一張JPEG圖片)

關於Key的一些注意事項:

  • 不要使用過長的Key。例如使用一個1024字節的key就不是一個好主意,不僅會消耗更多的內存,還會導致查找的效率降低
  • Key短到缺失了可讀性也是不好的,例如"u1000flw"比起"user:1000:followers"來說,節省了寥寥的存儲空間,卻引發了可讀性和可維護性上的麻煩
  • 最好使用統一的規範來設計Key,比如"object-type:id:attr",以這一規範設計出的Key可能是"user:1000"或"comment:1234:reply-to"
  • Redis允許的最大Key長度是512MB(對Value的長度限制也是512MB)

String

String是Redis的基礎數據類型,Redis沒有Int、Float、Boolean等數據類型的概念,所有的基本類型在Redis中都以String體現。

與String相關的常用命令:

  • SET:為一個key設置value,可以配合EX/PX參數指定key的有效期,通過NX/XX參數針對key是否存在的情況進行區別操作,時間複雜度O(1)
  • GET:獲取某個key對應的value,時間複雜度O(1)
  • GETSET:為一個key設置value,並返回該key的原value,時間複雜度O(1)
  • MSET:為多個key設置value,時間複雜度O(N)
  • MSETNX:同MSET,如果指定的key中有任意一個已存在,則不進行任何操作,時間複雜度O(N)
  • MGET:獲取多個key對應的value,時間複雜度O(N)

上文提到過,Redis的基本數據類型只有String,但Redis可以把String作為整型或浮點型數字來使用,主要體現在INCR、DECR類的命令上:

  • INCR:將key對應的value值自增1,並返回自增後的值。只對可以轉換為整型的String數據起作用。時間複雜度O(1)
  • INCRBY:將key對應的value值自增指定的整型數值,並返回自增後的值。只對可以轉換為整型的String數據起作用。時間複雜度O(1)
  • DECR/DECRBY:同INCR/INCRBY,自增改為自減。

INCR/DECR系列命令要求操作的value類型為String,並可以轉換為64位帶符號的整型數字,否則會返回錯誤。

也就是說,進行INCR/DECR系列命令的value,必須在[-2^63 ~ 2^63 - 1]範圍內。

前文提到過,Redis採用單線程模型,天然是線程安全的,這使得INCR/DECR命令可以非常便利的實現高併發場景下的精確控制。

例1:庫存控制

在高併發場景下實現庫存餘量的精準校驗,確保不出現超賣的情況。

設置庫存總量:

SET inv:remain "100"

庫存扣減+餘量校驗:

DECR inv:remain

當DECR命令返回值大於等於0時,說明庫存餘量校驗通過,如果返回小於0的值,則說明庫存已耗盡。

假設同時有300個併發請求進行庫存扣減,Redis能夠確保這300個請求分別得到99到-200的返回值,每個請求得到的返回值都是唯一的,絕對不會找出現兩個請求得到一樣的返回值的情況。

例2:自增序列生成

實現類似於RDBMS的Sequence功能,生成一系列唯一的序列號

設置序列起始值:

SET sequence "10000"

獲取一個序列值:

INCR sequence

直接將返回值作為序列使用即可。

獲取一批(如100個)序列值:

INCRBY sequence 100

假設返回值為N,那麼[N - 99 ~ N]的數值都是可用的序列值。

當多個客戶端同時向Redis申請自增序列時,Redis能夠確保每個客戶端得到的序列值或序列範圍都是全局唯一的,絕對不會出現不同客戶端得到了重複的序列值的情況。

List

Redis的List是鏈表型的數據結構,可以使用LPUSH/RPUSH/LPOP/RPOP等命令在List的兩端執行插入元素和彈出元素的操作。

雖然List也支持在特定index上插入和讀取元素的功能,但其時間複雜度較高(O(N)),應小心使用。

與List相關的常用命令:

  • LPUSH:向指定List的左側(即頭部)插入1個或多個元素,返回插入後的List長度。時間複雜度O(N),N為插入元素的數量
  • RPUSH:同LPUSH,向指定List的右側(即尾部)插入1或多個元素
  • LPOP:從指定List的左側(即頭部)移除一個元素並返回,時間複雜度O(1)
  • RPOP:同LPOP,從指定List的右側(即尾部)移除1個元素並返回
  • LPUSHX/RPUSHX:與LPUSH/RPUSH類似,區別在於,LPUSHX/RPUSHX操作的key如果不存在,則不會進行任何操作
  • LLEN:返回指定List的長度,時間複雜度O(1)
  • LRANGE:返回指定List中指定範圍的元素(雙端包含,即LRANGE key 0 10會返回11個元素),時間複雜度O(N)。


注意:應儘可能控制一次獲取的元素數量,一次獲取過大範圍的List元素會導致延遲,同時對長度不可預知的List,避免使用LRANGE key 0 -1這樣的完整遍歷操作。

應謹慎使用的List相關命令:

  • LINDEX:返回指定List指定index上的元素,如果index越界,返回nil。index數值是迴環的,即-1代表List最後一個位置,-2代表List倒數第二個位置。時間複雜度O(N)
  • LSET:將指定List指定index上的元素設置為value,如果index越界則返回錯誤,時間複雜度O(N),如果操作的是頭/尾部的元素,則時間複雜度為O(1)
  • LINSERT:向指定List中指定元素之前/之後插入一個新元素,並返回操作後的List長度。如果指定的元素不存在,返回-1。如果指定key不存在,不會進行任何操作,時間複雜度O(N)

由於Redis的List是鏈表結構的,上述的三個命令的算法效率較低,需要對List進行遍歷,命令的耗時無法預估,在List長度大的情況下耗時會明顯增加,應謹慎使用。

換句話說,Redis的List實際是設計來用於實現隊列,而不是用於實現類似ArrayList這樣的列表的。

如果你不是想要實現一個雙端出入的隊列,那麼請儘量不要使用Redis的List數據結構。

為了更好支持隊列的特性,Redis還提供了一系列阻塞式的操作命令,如BLPOP/BRPOP等,能夠實現類似於BlockingQueue的能力,即在List為空時,阻塞該連接,直到List中有對象可以出隊時再返回。

Hash

Hash即哈希表,Redis的Hash和傳統的哈希表一樣,是一種field-value型的數據結構,可以理解成將HashMap搬入Redis。

Hash非常適合用於表現對象類型的數據,用Hash中的field對應對象的field即可。

Hash的優點包括:

  • 可以實現二元查找,如"查找ID為1000的用戶的年齡"
  • 比起將整個對象序列化後作為String存儲的方法,Hash能夠有效地減少網絡傳輸的消耗
  • 當使用Hash維護一個集合時,提供了比List效率高得多的隨機訪問命令

與Hash相關的常用命令:

  • HSET:將key對應的Hash中的field設置為value。如果該Hash不存在,會自動創建一個。時間複雜度O(1)
  • HGET:返回指定Hash中field字段的值,時間複雜度O(1)


  • HMSET/HMGET:同HSET和HGET,可以批量操作同一個key下的多個field,時間複雜度:O(N),N為一次操作的field數量
  • HSETNX:同HSET,但如field已經存在,HSETNX不會進行任何操作,時間複雜度O(1)
  • HEXISTS:判斷指定Hash中field是否存在,存在返回1,不存在返回0,時間複雜度O(1)
  • HDEL:刪除指定Hash中的field(1個或多個),時間複雜度:O(N),N為操作的field數量
  • HINCRBY:同INCRBY命令,對指定Hash中的一個field進行INCRBY,時間複雜度O(1)

應謹慎使用的Hash相關命令:

  • HGETALL:返回指定Hash中所有的field-value對。返回結果為數組,數組中field和value交替出現。時間複雜度O(N)
  • HKEYS/HVALS:返回指定Hash中所有的field/value,時間複雜度O(N)

上述三個命令都會對Hash進行完整遍歷,Hash中的field數量與命令的耗時線性相關

對於尺寸不可預知的Hash,應嚴格避免使用上面三個命令,而改為使用HSCAN命令進行遊標式的遍歷

Set

Redis Set是無序的,不可重複的String集合。

與Set相關的常用命令:

  • SADD:向指定Set中添加1個或多個member,如果指定Set不存在,會自動創建一個。時間複雜度O(N),N為添加的member個數
  • SREM:從指定Set中移除1個或多個member,時間複雜度O(N),N為移除的member個數
  • SRANDMEMBER:從指定Set中隨機返回1個或多個member,時間複雜度O(N),N為返回的member個數
  • SPOP:從指定Set中隨機移除並返回count個member,時間複雜度O(N),N為移除的member個數
  • SCARD:返回指定Set中的member個數,時間複雜度O(1)
  • SISMEMBER:判斷指定的value是否存在於指定Set中,時間複雜度O(1)
  • SMOVE:將指定member從一個Set移至另一個Set

慎用的Set相關命令:

  • SMEMBERS:返回指定Hash中所有的member,時間複雜度O(N)
  • SUNION/SUNIONSTORE:計算多個Set的並集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數
  • SINTER/SINTERSTORE:計算多個Set的交集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數
  • SDIFF/SDIFFSTORE:計算1個Set與1或多個Set的差集並返回/存儲至另一個Set中,時間複雜度O(N),N為參與計算的所有集合的總member數

上述幾個命令涉及的計算量大,應謹慎使用,特別是在參與計算的Set尺寸不可知的情況下,應嚴格避免使用。

Sorted Set

Redis Sorted Set是有序的、不可重複的String集合。

Sorted Set中的每個元素都需要指派一個分數(score),Sorted Set會根據score對元素進行升序排序。

如果多個member擁有相同的score,則以字典序進行升序排序。Sorted Set非常適合用於實現排名。

Sorted Set的主要命令:

  • ZADD:向指定Sorted Set中添加1個或多個member,時間複雜度O(Mlog(N)),M為添加的member數量,N為Sorted Set中的member數量
  • ZREM:從指定Sorted Set中刪除1個或多個member,時間複雜度O(Mlog(N)),M為刪除的member數量,N為Sorted Set中的member數量
  • ZCOUNT:返回指定Sorted Set中指定score範圍內的member數量,時間複雜度:O(log(N))
  • ZCARD:返回指定Sorted Set中的member數量,時間複雜度O(1)
  • ZSCORE:返回指定Sorted Set中指定member的score,時間複雜度O(1)
  • ZRANK/ZREVRANK:返回指定member在Sorted Set中的排名,ZRANK返回按升序排序的排名,ZREVRANK則返回按降序排序的排名。時間複雜度O(log(N))
  • ZINCRBY:同INCRBY,對指定Sorted Set中的指定member的score進行自增,時間複雜度O(log(N))

慎用的Sorted Set相關命令:

  • ZRANGE/ZREVRANGE:返回指定Sorted Set中指定排名範圍內的所有member,ZRANGE為按score升序排序,ZREVRANGE為按score降序排序,時間複雜度O(log(N)+M),M為本次返回的member數
  • ZRANGEBYSCORE/ZREVRANGEBYSCORE:返回指定Sorted Set中指定score範圍內的所有member,返回結果以升序/降序排序,min和max可以指定為-inf和+inf,代表返回所有的member。時間複雜度O(log(N)+M)
  • ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除Sorted Set中指定排名範圍/指定score範圍內的所有member。時間複雜度O(log(N)+M)

上述幾個命令,應儘量避免傳遞[0 -1]或[-inf +inf]這樣的參數,來對Sorted Set做一次性的完整遍歷,特別是在Sorted Set的尺寸不可預知的情況下。

Bitmap和HyperLogLog

Redis的這兩種數據結構相較之前的並不常用,在本文中只做簡要介紹

Bitmap在Redis中不是一種實際的數據類型,而是一種將String作為Bitmap使用的方法。

可以理解為將String轉換為bit數組。使用Bitmap來存儲true/false類型的簡單數據極為節省空間。

HyperLogLogs是一種主要用於數量統計的數據結構,它和Set類似,維護一個不可重複的String集合

但是HyperLogLogs並不維護具體的member內容,只維護member的個數。

也就是說,HyperLogLogs只能用於計算一個集合中不重複的元素數量,所以它比Set要節省很多內存空間。

其他常用命令

  • EXISTS:判斷指定的key是否存在,返回1代表存在,0代表不存在,時間複雜度O(1)
  • DEL:刪除指定的key及其對應的value,時間複雜度O(N),N為刪除的key數量
  • EXPIRE/PEXPIRE:為一個key設置有效期,單位為秒或毫秒,時間複雜度O(1)
  • TTL/PTTL:返回一個key剩餘的有效時間,單位為秒或毫秒,時間複雜度O(1)
  • RENAME/RENAMENX:將key重命名為newkey。使用RENAME時,如果newkey已經存在,其值會被覆蓋;使用RENAMENX時,如果newkey已經存在,則不會進行任何操作,時間複雜度O(1)
  • TYPE:返回指定key的類型,string, list, set, zset, hash。時間複雜度O(1)
  • CONFIG GET:獲得Redis某配置項的當前值,可以使用*通配符,時間複雜度O(1)
  • CONFIG SET:為Redis某個配置項設置新值,時間複雜度O(1)
  • CONFIG REWRITE:讓Redis重新加載redis.conf中的配置

數據持久化

Redis提供了將數據定期自動持久化至硬盤的能力,包括RDB和AOF兩種方案。

兩種方案分別有其長處和短板,可以配合起來同時運行,確保數據的穩定性。

必須使用數據持久化嗎?

Redis的數據持久化機制是可以關閉的,如果你只把Redis作為緩存服務使用,Redis中存儲的所有數據都不是該數據的主體而僅僅是同步過來的備份,那麼可以關閉Redis的數據持久化機制。

但通常來說,仍然建議至少開啟RDB方式的數據持久化,因為:

  • RDB方式的持久化幾乎不損耗Redis本身的性能,在進行RDB持久化時,Redis主進程唯一需要做的事情就是fork出一個子進程,所有持久化工作都由子進程完成
  • Redis無論因為什麼原因crash掉之後,重啟時能夠自動恢復到上一次RDB快照中記錄的數據。這省去了手工從其他數據源(如DB)同步數據的過程,而且要比其他任何的數據恢復方式都要快
  • 現在硬盤那麼大,真的不缺那一點地方

RDB

採用RDB持久方式,Redis會定期保存數據快照至一個rbd文件中,並在啟動時自動加載rdb文件,恢復之前保存的數據。

可以在配置文件中配置Redis進行快照保存的時機:

save [seconds] [changes]

意為在[seconds]秒內如果發生了[changes]次數據修改,則進行一次RDB快照保存,例如:

save 60 100

會讓Redis每60秒檢查一次數據變更情況,如果發生了100次或以上的數據變更,則進行RDB快照保存。

可以配置多條save指令,讓Redis執行多級的快照保存策略。

Redis默認開啟RDB快照,默認的RDB策略如下:

save 900 1
save 300 10
save 60 10000

也可以通過BGSAVE命令手工觸發RDB快照保存。

RDB的優點:

  • 對性能影響最小。如前文所述,Redis在保存RDB快照時會fork出子進程進行,幾乎不影響Redis處理客戶端請求的效率。
  • 每次快照會生成一個完整的數據快照文件,所以可以輔以其他手段保存多個時間點的快照(例如把每天0點的快照備份至其他存儲媒介中),作為非常可靠的災難恢復手段。
  • 使用RDB文件進行數據恢復比使用AOF要快很多。

RDB的缺點:

  • 快照是定期生成的,所以在Redis crash時或多或少會丟失一部分數據。
  • 如果數據集非常大且CPU不夠強(比如單核CPU),Redis在fork子進程時可能會消耗相對較長的時間(長至1秒),影響這期間的客戶端請求。

AOF

採用AOF持久方式時,Redis會把每一個寫請求都記錄在一個日誌文件裡。

在Redis重啟時,會把AOF文件中記錄的所有寫操作順序執行一遍,確保數據恢復到最新。

AOF默認是關閉的,如要開啟,進行如下配置:

appendonly yes

AOF提供了三種fsync配置,always/everysec/no,通過配置項[appendfsync]指定:

  • appendfsync no:不進行fsync,將flush文件的時機交給OS決定,速度最快
  • appendfsync always:每寫入一條日誌就進行一次fsync操作,數據安全性最高,但速度最慢
  • appendfsync everysec:折中的做法,交由後臺線程每秒fsync一次

隨著AOF不斷地記錄寫操作日誌,必定會出現一些無用的日誌

例如某個時間點執行了命令SET key1 "abc",在之後某個時間點又執行了SET key1 "bcd",那麼第一條命令很顯然是沒有用的。

大量的無用日誌會讓AOF文件過大,也會讓數據恢復的時間過長。

所以Redis提供了AOF rewrite功能,可以重寫AOF文件,只保留能夠把數據恢復到最新狀態的最小寫操作集。

AOF rewrite可以通過BGREWRITEAOF命令觸發,也可以配置Redis定期自動進行:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

上面兩行配置的含義是,Redis在每次AOF rewrite時,會記錄完成rewrite後的AOF日誌大小,當AOF日誌大小在該基礎上增長了100%後,自動進行AOF rewrite。同時如果增長的大小沒有達到64mb,則不會進行rewrite。

AOF的優點:

  • 最安全,在啟用appendfsync always時,任何已寫入的數據都不會丟失,使用在啟用appendfsync everysec也至多隻會丟失1秒的數據。
  • AOF文件在發生斷電等問題時也不會損壞,即使出現了某條日誌只寫入了一半的情況,也可以使用redis-check-aof工具輕鬆修復。
  • AOF文件易讀,可修改,在進行了某些錯誤的數據清除操作後,只要AOF文件沒有rewrite,就可以把AOF文件備份出來,把錯誤的命令刪除,然後恢復數據。

AOF的缺點:

  • AOF文件通常比RDB文件更大
  • 性能消耗比RDB高
  • 數據恢復速度比RDB慢

內存管理與數據淘汰機制

最大內存設置

默認情況下,在32位OS中,Redis最大使用3GB的內存,在64位OS中則沒有限制。

在使用Redis時,應該對數據佔用的最大空間有一個基本準確的預估,併為Redis設定最大使用的內存。

否則在64位OS中Redis會無限制地佔用內存(當物理內存被佔滿後會使用swap空間),容易引發各種各樣的問題。

通過如下配置控制Redis使用的最大內存:

maxmemory 100mb

在內存佔用達到了maxmemory後,再向Redis寫入數據時,Redis會:

  • 根據配置的數據淘汰策略嘗試淘汰數據,釋放空間
  • 如果沒有數據可以淘汰,或者沒有配置數據淘汰策略,那麼Redis會對所有寫請求返回錯誤,但讀請求仍然可以正常執行

在為Redis設置maxmemory時,需要注意:


如果採用了Redis的主從同步,主節點向從節點同步數據時,會佔用掉一部分內存空間

如果maxmemory過於接近主機的可用內存,會導致數據同步時內存不足。

所以設置的maxmemory不要過於接近主機可用的內存,留出一部分預留用作主從同步。

數據淘汰機制

Redis提供了5種數據淘汰策略:

  • volatile-lru:使用LRU算法進行數據淘汰(淘汰上次使用時間最早的,且使用次數最少的key),只淘汰設定了有效期的key
  • allkeys-lru:使用LRU算法進行數據淘汰,所有的key都可以被淘汰
  • volatile-random:隨機淘汰數據,只淘汰設定了有效期的key
  • allkeys-random:隨機淘汰數據,所有的key都可以被淘汰
  • volatile-ttl:淘汰剩餘有效期最短的key

最好為Redis指定一種有效的數據淘汰策略以配合maxmemory設置,避免在內存使用滿後發生寫入失敗的情況。

一般來說,推薦使用的策略是volatile-lru,並辨識Redis中保存的數據的重要性。

對於那些重要的,絕對不能丟棄的數據(如配置類數據等),應不設置有效期,這樣Redis就永遠不會淘汰這些數據。

對於那些相對不是那麼重要的,並且能夠熱加載的數據(比如緩存最近登錄的用戶信息,當在Redis中找不到時,程序會去DB中讀取),可以設置上有效期,這樣在內存不夠時Redis就會淘汰這部分數據。

配置方法:

maxmemory-policy volatile-lru #默認是noeviction,即不進行數據淘汰

Pipelining

Pipelining

Redis提供許多批量操作的命令,如MSET/MGET/HMSET/HMGET等等,這些命令存在的意義是減少維護網絡連接和傳輸數據所消耗的資源和時間。

例如連續使用5次SET命令設置5個不同的key,比起使用一次MSET命令設置5個不同的key,效果是一樣的

但前者會消耗更多的RTT(Round Trip Time)時長,永遠應優先使用後者。

然而,如果客戶端要連續執行的多次操作無法通過Redis命令組合在一起,例如:

SET a "abc"
INCR b
HSET c name "hi"

此時便可以使用Redis提供的pipelining功能來實現在一次交互中執行多條命令。

使用pipelining時,只需要從客戶端一次向Redis發送多條命令(以\\r\\n)分隔,Redis就會依次執行這些命令,並且把每個命令的返回按順序組裝在一起一次返回,比如:

$ (printf "PING\\r\\nPING\\r\\nPING\\r\\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

大部分的Redis客戶端都對Pipelining提供支持,所以開發者通常並不需要自己手工拼裝命令列表。

Pipelining的侷限性

Pipelining只能用於執行連續且無相關性的命令,當某個命令的生成需要依賴於前一個命令的返回時,就無法使用Pipelining了。

通過Scripting功能,可以規避這一侷限性

事務與Scripting

Pipelining能夠讓Redis在一次交互中處理多條命令,然而在一些場景下,我們可能需要在此基礎上確保這一組命令是連續執行的。

比如獲取當前累計的PV數並將其清0

> GET vCount
12384
> SET vCount 0
OK

如果在GET和SET命令之間插進來一個INCR vCount,就會使客戶端拿到的vCount不準確。

Redis的事務可以確保複數命令執行時的原子性。

也就是說Redis能夠保證:一個事務中的一組命令是絕對連續執行的,在這些命令執行完成之前,絕對不會有來自於其他連接的其他命令插進去執行。

通過MULTI和EXEC命令來把這兩個命令加入一個事務中:

> MULTI
OK
> GET vCount
QUEUED
> SET vCount 0
QUEUED
> EXEC
1) 12384
2) OK

Redis在接收到MULTI命令後便會開啟一個事務,這之後的所有讀寫命令都會保存在隊列中但並不執行

直到接收到EXEC命令後,Redis會把隊列中的所有命令連續順序執行,並以數組形式返回每個命令的返回結果。

可以使用DISCARD命令放棄當前的事務,將保存的命令隊列清空。

需要注意的是,Redis事務不支持回滾:如果一個事務中的命令出現了語法錯誤,大部分客戶端驅動會返回錯誤

2.6.5版本以上的Redis也會在執行EXEC時檢查隊列中的命令是否存在語法錯誤,如果存在,則會自動放棄事務並返回錯誤。

但如果一個事務中的命令有非語法類的錯誤(比如對String執行HSET操作),無論客戶端驅動還是Redis都無法在真正執行這條命令之前發現,所以事務中的所有命令仍然會被依次執行。

在這種情況下,會出現一個事務中部分命令成功部分命令失敗的情況,然而與RDBMS不同,Redis不提供事務回滾的功能,所以只能通過其他方法進行數據的回滾。

通過事務實現CAS

Redis提供了WATCH命令與事務搭配使用,實現CAS樂觀鎖的機制。

假設要實現將某個商品的狀態改為已售:

if(exec(HGET stock:1001 state) == "in stock")
exec(HSET stock:1001 state "sold");

這一偽代碼執行時,無法確保併發安全性,有可能多個客戶端都獲取到了"in stock"的狀態,導致一個庫存被售賣多次。

使用WATCH命令和事務可以解決這一問題:

exec(WATCH stock:1001);
if(exec(HGET stock:1001 state) == "in stock") {
exec(MULTI);
exec(HSET stock:1001 state "sold");
exec(EXEC);
}

WATCH的機制是:在事務EXEC命令執行時,Redis會檢查被WATCH的key,只有被WATCH的key從WATCH起始時至今沒有發生過變更,EXEC才會被執行。

如果WATCH的key在WATCH命令到EXEC命令之間發生過變化,則EXEC命令會返回失敗。

Scripting

通過EVAL與EVALSHA命令,可以讓Redis執行LUA腳本。這就類似於RDBMS的存儲過程一樣,可以把客戶端與Redis之間密集的讀/寫交互放在服務端進行,避免過多的數據交互,提升性能。

Scripting功能是作為事務功能的替代者誕生的,事務提供的所有能力Scripting都可以做到。Redis官方推薦使用LUA Script來代替事務,前者的效率和便利性都超過了事務。

關於Scripting的具體使用,本文不做詳細介紹,請參考官方文檔 https://redis.io/commands/eval

Redis性能調優

儘管Redis是一個非常快速的內存數據存儲媒介,也並不代表Redis不會產生性能問題。

前文中提到過,Redis採用單線程模型,所有的命令都是由一個線程串行執行的

所以當某個命令執行耗時較長時,會拖慢其後的所有命令,這使得Redis對每個任務的執行效率更加敏感。

針對Redis的性能優化,主要從下面幾個層面入手:

  • 最初的也是最重要的,確保沒有讓Redis執行耗時長的命令
  • 使用pipelining將連續執行的命令組合執行
  • 操作系統的Transparent huge pages功能必須關閉:
echo never > /sys/kernel/mm/transparent_hugepage/enabled

  • 如果在虛擬機中運行Redis,可能天然就有虛擬機環境帶來的固有延遲。
  • 可以通過./redis-cli --intrinsic-latency 100命令查看固有延遲。同時如果對Redis的性能有較高要求的話,應儘可能在物理機上直接部署Redis。
  • 檢查數據持久化策略
  • 考慮引入讀寫分離機制
你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)

長耗時命令

Redis絕大多數讀寫命令的時間複雜度都在O(1)到O(N)之間,在本文和官方文檔中均對每個命令的時間複雜度有說明。

通常來說,O(1)的命令是安全的,O(N)命令在使用時需要注意,如果N的數量級不可預知,則應避免使用。

例如對一個field數未知的Hash數據執行HGETALL/HKEYS/HVALS命令,通常來說這些命令執行的很快,但如果這個Hash中的field數量極多,耗時就會成倍增長。

又如使用SUNION對兩個Set執行Union操作,或使用SORT對List/Set執行排序操作等時,都應該嚴加註意。

避免在使用這些O(N)命令時發生問題主要有幾個辦法:

  • 不要把List當做列表使用,僅當做隊列來使用
  • 通過機制嚴格控制Hash、Set、Sorted Set的大小
  • 可能的話,將排序、並集、交集等操作放在客戶端執行
  • 絕對禁止使用KEYS命令
  • 避免一次性遍歷集合類型的所有成員,而應使用SCAN類的命令進行分批的,遊標式的遍歷


Redis提供了SCAN命令,可以對Redis中存儲的所有key進行遊標式的遍歷,避免使用KEYS命令帶來的性能問題。

同時還有SSCAN/HSCAN/ZSCAN等命令,分別用於對Set/Hash/Sorted Set中的元素進行遊標式遍歷。

SCAN類命令的使用請參考官方文檔:

https://redis.io/commands/scan

Redis提供了Slow Log功能,可以自動記錄耗時較長的命令。相關的配置參數有兩個:

slowlog-log-slower-than xxxms #執行時間慢於xxx毫秒的命令計入Slow Log
slowlog-max-len xxx #Slow Log的長度,即最大紀錄多少條Slow Log

使用SLOWLOG GET [number]命令,可以輸出最近進入Slow Log的number條命令。

使用SLOWLOG RESET命令,可以重置Slow Log

網絡引發的延遲

  • 儘可能使用長連接或連接池,避免頻繁創建銷燬連接
  • 客戶端進行的批量數據操作,應使用Pipeline特性在一次交互中完成。具體請參照本文的Pipelining章節

數據持久化引發的延遲

Redis的數據持久化工作本身就會帶來延遲,需要根據數據的安全級別和性能要求制定合理的持久化策略:

  • AOF + fsync always的設置雖然能夠絕對確保數據安全,但每個操作都會觸發一次fsync,會對Redis的性能有比較明顯的影響。
  • AOF + fsync every second是比較好的折中方案,每秒fsync一次
  • AOF + fsync never會提供AOF持久化方案下的最優性能
  • 使用RDB持久化通常會提供比使用AOF更高的性能,但需要注意RDB的策略配置
  • 每一次RDB快照和AOF Rewrite都需要Redis主進程進行fork操作。fork操作本身可能會產生較高的耗時,與CPU和Redis佔用的內存大小有關。根據具體的情況合理配置RDB快照和AOF Rewrite時機,避免過於頻繁的fork帶來的延遲

Redis在fork子進程時需要將內存分頁表拷貝至子進程以佔用了24GB內存的Redis實例為例,共需要拷貝24GB / 4kB * 8 = 48MB的數據。在使用單Xeon 2.27Ghz的物理機上,這一fork操作耗時216ms。可以通過INFO命令返回的latest_fork_usec字段查看上一次fork操作的耗時(微秒)

Swap引發的延遲

當Linux將Redis所用的內存分頁移至swap空間時,將會阻塞Redis進程,導致Redis出現不正常的延遲。

Swap通常在物理內存不足或一些進程在進行大量I/O操作時發生,應儘可能避免上述兩種情況的出現。

/proc//smaps文件中會保存進程的swap記錄,通過查看這個文件,能夠判斷Redis的延遲是否由Swap產生。

如果這個文件中記錄了較大的Swap size,則說明延遲很有可能是Swap造成的。

數據淘汰引發的延遲

當同一秒內有大量key過期時,也會引發Redis的延遲。在使用時應儘量將key的失效時間錯開。

引入讀寫分離機制

Redis的主從複製能力可以實現一主多從的多節點架構,在這一架構下,主節點接收所有寫請求,並將數據同步給多個從節點。

在這一基礎上,我們可以讓從節點提供對實時性要求不高的讀請求服務,以減小主節點的壓力。

尤其是針對一些使用了長耗時命令的統計類任務,完全可以指定在一個或多個從節點上執行,避免這些長耗時命令影響其他請求的響應。

主從複製與集群分片

主從複製

Redis支持一主多從的主從複製架構。一個Master實例負責處理所有的寫請求,Master將寫操作同步至所有Slave。

藉助Redis的主從複製,可以實現讀寫分離和高可用:

  • 實時性要求不是特別高的讀請求,可以在Slave上完成,提升效率。特別是一些週期性執行的統計任務,這些任務可能需要執行一些長耗時的Redis命令,可以專門規劃出1個或幾個Slave用於服務這些統計任務
  • 藉助Redis Sentinel可以實現高可用,當Master crash後,Redis Sentinel能夠自動將一個Slave晉升為Master,繼續提供服務

啟用主從複製非常簡單,只需要配置多個Redis實例,在作為Slave的Redis實例中配置:

slaveof 192.168.1.1 6379 #指定Master的IP和端口

當Slave啟動後,會從Master進行一次冷啟動數據同步

由Master觸發BGSAVE生成RDB文件推送給Slave進行導入,導入完成後Master再將增量數據通過Redis Protocol同步給Slave

之後主從之間的數據便一直以Redis Protocol進行同步

使用Sentinel做自動failover

Redis的主從複製功能本身只是做數據同步,並不提供監控和自動failover能力

要通過主從複製功能來實現Redis的高可用,還需要引入一個組件:Redis Sentinel

Redis Sentinel是Redis官方開發的監控組件,可以監控Redis實例的狀態,通過Master節點自動發現Slave節點,並在監測到Master節點失效時選舉出一個新的Master,並向所有Redis實例推送新的主從配置。

Redis Sentinel需要至少部署3個實例才能形成選舉關係。

關鍵配置:

sentinel monitor mymaster 127.0.0.1 6379 2 #Master實例的IP、端口,以及選舉需要的贊成票數
sentinel down-after-milliseconds mymaster 60000 #多長時間沒有響應視為Master失效
sentinel failover-timeout mymaster 180000 #兩次failover嘗試間的間隔時長
sentinel parallel-syncs mymaster 1 #如果有多個Slave,可以通過此配置指定同時從新Master進行數據同步的Slave數,避免所有Slave同時進行數據同步導致查詢服務也不可用

另外需要注意的是,Redis Sentinel實現的自動failover不是在同一個IP和端口上完成的

也就是說自動failover產生的新Master提供服務的IP和端口與之前的Master是不一樣的,所以要實現HA,還要求客戶端必須支持Sentinel,能夠與Sentinel交互獲得新Master的信息才行。

集群分片

為何要做集群分片:

  • Redis中存儲的數據量大,一臺主機的物理內存已經無法容納
  • Redis的寫請求併發量大,一個Redis實例以無法承載

當上述兩個問題出現時,就必須要對Redis進行分片了。

Redis的分片方案有很多種,例如很多Redis的客戶端都自行實現了分片功能,也有像Twemproxy這樣的以代理方式實現的Redis分片方案。

然而首選的方案還應該是Redis官方在3.0版本中推出的Redis Cluster分片方案。

本文不會對Redis Cluster的具體安裝和部署細節進行介紹,重點介紹Redis Cluster帶來的好處與弊端。

Redis Cluster的能力

  • 能夠自動將數據分散在多個節點上
  • 當訪問的key不在當前分片上時,能夠自動將請求轉發至正確的分片
  • 當集群中部分節點失效時仍能提供服務

其中第三點是基於主從複製來實現的,Redis Cluster的每個數據分片都採用了主從複製的結構,原理和前文所述的主從複製完全一致

唯一的區別是省去了Redis Sentinel這一額外的組件,由Redis Cluster負責進行一個分片內部的節點監控和自動failover。

Redis Cluster分片原理

Redis Cluster中共有16384個hash slot,Redis會計算每個key的CRC16,將結果與16384取模,來決定該key存儲在哪一個hash slot中,

同時需要指定Redis Cluster中每個數據分片負責的Slot數,Slot的分配在任何時間點都可以進行重新分配。

客戶端在對key進行讀寫操作時,可以連接Cluster中的任意一個分片,如果操作的key不在此分片負責的Slot範圍內,Redis Cluster會自動將請求重定向到正確的分片上。

hash tags

在基礎的分片原則上,Redis還支持hash tags功能,以hash tags要求的格式明明的key,將會確保進入同一個Slot中。

例如:{uiv}user:1000和{uiv}user:1001擁有同樣的hash tag {uiv},會保存在同一個Slot中。

使用Redis Cluster時,pipelining、事務和LUA Script功能涉及的key必須在同一個數據分片上,否則將會返回錯誤。

如要在Redis Cluster中使用上述功能,就必須通過hash tags來確保一個pipeline或一個事務中操作的所有key都位於同一個Slot中。

有一些客戶端(如Redisson)實現了集群化的pipelining操作,可以自動將一個pipeline裡的命令按key所在的分片進行分組,分別發到不同的分片上執行。但是Redis不支持跨分片的事務,事務和LUA Script還是必須遵循所有key在一個分片上的規則要求。

主從複製 vs 集群分片

在設計軟件架構時,要如何在主從複製和集群分片兩種部署方案中取捨呢?

從各個方面看,Redis Cluster都是優於主從複製的方案

  • Redis Cluster能夠解決單節點上數據量過大的問題
  • Redis Cluster能夠解決單節點訪問壓力過大的問題
  • Redis Cluster包含了主從複製的能力


那是不是代表Redis Cluster永遠是優於主從複製的選擇呢?

並不是!!!

軟件架構永遠不是越複雜越好,複雜的架構在帶來顯著好處的同時,一定也會帶來相應的弊端。採用Redis Cluster的弊端包括:

1、維護難度增加

在使用Redis Cluster時,需要維護的Redis實例數倍增,需要監控的主機數量也相應增加,數據備份/持久化的複雜度也會增加。

同時在進行分片的增減操作時,還需要進行reshard操作,遠比主從模式下增加一個Slave的複雜度要高。

2、客戶端資源消耗增加

當客戶端使用連接池時,需要為每一個數據分片維護一個連接池,客戶端同時需要保持的連接數成倍增多,加大了客戶端本身和操作系統資源的消耗。

3、性能優化難度增加

你可能需要在多個分片上查看Slow Log和Swap日誌才能定位性能問題。

4、事務和LUA Script的使用成本增加

在Redis Cluster中使用事務和LUA Script特性有嚴格的限制條件

事務和Script中操作的key必須位於同一個分片上,這就使得在開發時必須對相應場景下涉及的key進行額外的規劃和規範要求。

如果應用的場景中大量涉及事務和Script的使用,如何在保證這兩個功能的正常運作前提下把數據平均分到多個數據分片中就會成為難點。

所以,在主從複製和集群分片兩個方案中做出選擇時,應該從應用軟件的功能特性、數據和訪問量級、未來發展規劃等方面綜合考慮,只在確實有必要引入數據分片時再使用Redis Cluster。

下面是一些建議:

  • 需要在Redis中存儲的數據有多大?未來2年內可能發展為多大?這些數據是否都需要長期保存?是否可以使用LRU算法進行非熱點數據的淘汰?綜合考慮前面幾個因素,評估出Redis需要使用的物理內存。
  • 用於部署Redis的主機物理內存有多大?有多少可以分配給Redis使用?對比(1)中的內存需求評估,是否足夠用?
  • Redis面臨的併發寫壓力會有多大?在不使用pipelining時,Redis的寫性能可以超過10萬次/秒(更多的benchmark可以參考 https://redis.io/topics/benchmarks )
  • 在使用Redis時,是否會使用到pipelining和事務功能?使用的場景多不多?

綜合上面幾點考慮,如果單臺主機的可用物理內存完全足以支撐對Redis的容量需求,且Redis面臨的併發寫壓力距離Benchmark值還尚有距離,建議採用主從複製的架構,可以省去很多不必要的麻煩。

同時,如果應用中大量使用pipelining和事務,也建議儘可能選擇主從複製架構,可以減少設計和開發時的複雜度。

Redis Java客戶端的選擇

Redis的Java客戶端很多,官方推薦的有三種:Jedis、Redisson和lettuce。

在這裡對Jedis和Redisson進行對比介紹

Jedis:

  • 輕量,簡潔,便於集成和改造
  • 支持連接池
  • 支持pipelining、事務、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持讀寫分離,需要自己實現
  • 文檔差(真的很差,幾乎沒有……)

Redisson:

  • 基於Netty實現,採用非阻塞IO,性能高
  • 支持異步請求
  • 支持連接池
  • 支持pipelining、LUA Scripting、Redis Sentinel、Redis Cluster
  • 不支持事務,官方建議以LUA Scripting代替事務
  • 支持在Redis Cluster架構下使用pipelining
  • 支持讀寫分離,支持讀負載均衡,在主從複製和Redis Cluster架構下都可以使用
  • 內建Tomcat Session Manager,為Tomcat 6/7/8提供了會話共享功能
  • 可以與Spring Session集成,實現基於Redis的會話共享
  • 文檔較豐富,有中文文檔

對於Jedis和Redisson的選擇,同樣應遵循前述的原理,儘管Jedis比起Redisson有各種各樣的不足,但也應該在需要使用Redisson的高級特性時再選用Redisson,避免造成不必要的程序複雜度提升。

其實很多技術不是一兩篇文章就能說的清楚的。

那如何快速有效的學習並精通呢?

轉發!

轉發!

轉發後關注作者,私信關鍵詞:“ Java ”

以下價值1990元資料,免費領取!

你的項目中有Redis嗎?Get完這篇你能掌握Redis60%(推薦收藏)

"

相關推薦

推薦中...