Redis基礎、進階特性與效能調優
kelgon 關注 2017.02.28 16:22 字數 12597 閱讀 16333 評論 5 喜歡 154 讚賞 2
本文將從Redis的基本特性入手,通過講述Redis的資料結構和主要命令對Redis的基本能力進行直觀介紹。之後概覽Redis提供的進階能力,並在部署、維護、效能調優等多個方面進行更深入的介紹和指導。
本文適合使用Redis的普通開發人員,以及對Redis進行選型、架構設計和效能調優的架構設計人員。 目錄 概述 Redis的資料結構和相關常用命令 資料持久化 記憶體管理與資料淘汰機制 Pipelining 事務與Scripting Redis效能調優 主從複製與叢集分區 Redis Java用戶端的選擇 概述
Redis是一個開源的,基於記憶體的結構化資料存放區媒介,可以作為資料庫、快取服務或Message Service使用。
Redis支援多種資料結構,包括字串、雜湊表、鏈表、集合、有序集合、位元影像、Hyperloglogs等。
Redis具備LRU淘汰、事務實現、以及不同層級的硬碟持久化等能力,並且支援複本集和通過Redis Sentinel實現的高可用方案,同時還支援通過Redis Cluster實現的資料自動分區能力。
Redis的主要功能都基於單執行緒模式實現,也就是說Redis使用一個線程來服務所有的用戶端請求,同時Redis採用了非阻塞式IO,並精細地最佳化各種命令的演算法時間複雜度,這些資訊意味著: Redis是安全執行緒的(因為只有一個線程),其所有操作都是原子的,不會因並發產生資料異常 Redis的速度非常快(因為使用非阻塞式IO,且大部分命令的演算法時間複雜度都是O(1)) 使用高耗時的Redis命令是很危險的,會佔用唯一的一個線程的大量處理時間,導致所有的請求都被拖慢。(例如時間複雜度為O(N)的KEYS命令,嚴格禁止在生產環境中使用) Redis的資料結構和相關常用命令
本節中將介紹Redis支援的主要資料結構,以及相關的常用Redis命令。本節只對Redis命令進行扼要的介紹,且只列出了較常用的命令。如果想要瞭解完整的Redis命令集,或瞭解某個命令的詳細使用方法,請參考官方文檔:https://redis.io/commands 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的基礎資料型別 (Elementary Data Type)只有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中有對象可以出隊時再返回。針對阻塞類的命令,此處不做詳細探討,請參考官方文檔(https://redis.io/topics/data-types-intro) 中"Blocking operations on lists"一節。 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命令進行遊標式的遍曆,具體請見 https://redis.io/commands/scan 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尺寸不可知的情況下,應嚴格避免使用。可以考慮通過SSCAN命令遍曆擷取相關Set的全部member(具體請見 https://redis.io/commands/scan ),如果需要做並集/交集/差集計算,可以在用戶端進行,或在不服務即時查詢請求的Slave上進行。 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的尺寸不可預知的情況下。可以通過ZSCAN命令來進行遊標式的遍曆(具體請見 https://redis.io/commands/scan ),或通過LIMIT參數來限制返回member的數量(適用於ZRANGEBYSCORE和ZREVRANGEBYSCORE命令),以實現遊標式的遍曆。 Bitmap和HyperLogLog
Redis的這兩種資料結構相較之前的並不常用,在本文中只做簡要介紹,如想要詳細瞭解這兩種資料結構與其相關的命令,請參考官方文檔https://redis.io/topics/data-types-intro中的相關章節
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 1save 300 10save 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 100auto-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使用的最大記憶體: