3-4、事件功能和配置項
Redis從2.X版本開始,就支持一種基於非持久化消息的、使用發布/訂閱模式實現的事件通知機制。所謂基於非連接保持,是因為一旦消息訂閱者由於各種異常情況而被迫斷開連接,在其重新連接后,其離線期間的事件是無法被重新通知的(一些Redis資料中也稱為即發即棄)。而其使用的發布/訂閱模式,意味着其機制並不是由訂閱者周期性的從Redis服務拉取事件通知,而是由Redis服務主動推送事件通知到符合條件的若干訂閱者。
Redis中的事件功能可以提供兩種不同的功能。一類是基於Channel的消息事件,這一類消息和Redis中存儲的Keys沒有太多關聯,也就是說即使不在Redis中存儲任何Keys信息,這類消息事件也可以獨立使用。另一類消息事件可以對(也可以不對)Redis中存儲的Keys信息的變化事件進行通知,可以用來向訂閱者通知Redis中符合訂閱條件的Keys的各種事件。Redis服務的事件功能在實際場景中雖然使用得不多,不過還是可以找到案例,例如服務治理框架DUBBO默認情況下使用Zookeeper作為各節點的服務協調裝置,但可以通過更改DUBBO的配置,將Zookeeper更換為Redis。
3-4-1、publish和subscribe
我們先從比較簡單的publish命令和subscribe命令開始介紹,因為這組命令所涉及到的Channel(通道)和Redis中存儲的數據相對獨立。publish命令由發送者使用,負責向指定的Channel發送消息;subscribe命令由訂閱者使用,負責從指定的一個或者多個Channel中獲取消息。
以下是publish命令和subscribe命令的使用示例:
// 該命令向指定的channel名字發送一條消息(字符串)
PUBLISH channel message
// 例如:向名叫FM955的頻道發送一條消息,消息信息為“hello!”
PUBLISH FM955 "hello!" // 再例如:向名叫FM900的頻道發送一條消息,消息信息為“ doit!” PUBLISH FM900 "doit!" // 該命令可以開始向指定的一個或者多個channel訂閱消息 SUBSCRIBE channel [channel ...] // 例如:向名叫FM955的頻道訂閱消息 SUBSCRIBE FM955 // 再例如:向名叫FM955、FM900的兩個頻道訂閱消息 SUBSCRIBE FM955 FM900
如果您使用需要使用publish命令和subscribe命令,您並不需要對Redis服務的配置信息做任何更改。以下示例將向讀者展示兩個命令的簡單使用方式——前提是您的Redis服務已經啟動好了:
- 由客戶端A充當訂閱者,在ChannelA和ChannelB兩個通道上訂閱消息
-- 我們使用的Redis服務地址為192.168.61.140,端口為默認值 [root@kp2 ~]# redis-cli -h 192.168.61.140 192.168.61.140:6379> SUBSCRIBE ChannelA ChannelB Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "ChannelA" 3) (integer) 1 1) "subscribe" 2) "ChannelB" 3) (integer) 2
- 有客戶端B從當訂閱者,通過ChannelB發送消息給所有訂閱者。
-- 連接到Redis服務器后,直接運行PUBLISH命令,發送信息 [root@kp1 ~]# redis-cli -h 192.168.61.140 192.168.61.140:6379> PUBLISH ChannelB "hello" (integer) 1
- 以下是訂閱者客戶端A所受到的message信息:
...... -- 這時訂閱者收到消息如下: 1) "message" 2) "ChannelB" 3) "hello"
從以上示例中可以看到,客戶端A確實收到了客戶端B所發送的消息信息,並且收到三行信息。這三行信息分別表示消息類型、消息通道和消息內容。注意,以上介紹的這組publish命令和subscribe命令的操作過程並沒有對Redis服務中已存儲的任何Keys信息產生影響。
3-4-2、模式訂閱psubscribe
Redis中還支持一種模式訂閱,它主要依靠psubscribe命令向技術人員提供訂閱功能。模式訂閱psubscribe最大的特點是,它除了可以通過Channel訂閱消息以外,還可以配合配置命令來進行Keys信息變化的事件通知。
模式訂閱psubscribe的Channel訂閱和subscribe命令類似,這里給出一個命令格式,就不再多做介紹了(可參考上文對subscribe命令的介紹):
// 該命令可以開始向指定的一個或者多個channel訂閱消息
// 具體使用示例可參見SUBSCRIBE命令
PSUBSCRIBE channel [channel ...]
模式訂閱psubscribe對Keys變化事件的支持分為兩種類型:keyspace(鍵空間通知)和keyevent(鍵事件通知),這兩類事件都是依靠Key的變化觸發的,而關鍵的區別在於事件描述的焦點,舉例說明:
當Redis服務中0號數據庫的MyKey鍵被刪除時,鍵空間和鍵事件向模式訂閱者分別發送的消息格式如下:
// 以下命令可訂閱鍵空間通知 // 訂閱0號數據庫任何Key信息的變化 192.168.61.140:6379> psubscribe __keyspace@0__:* Reading messages... (press Ctrl-C to quit) 1) "psubscribe" 2) "__keyspace@0__:*" 3) (integer) 1 // 出現以上信息,說明訂閱成功 // 當其他客戶端執行 set mykey 123456 時,該訂閱可收到以下信息 1) "pmessage" 2) "__keyspace@0__:*" 3) "__keyspace@0__:mykey" 4) "set"
以上收到的訂閱信息,其描述可以概括為:“mykey的鍵空間發生了事件,事件為set”。這樣的事件描述着重於key的名稱,並且告訴客戶端key的事件為set。我們再來看看訂閱鍵事件通知時,發生同樣事件所得到的訂閱信息:
// 以下命令可訂閱鍵事件通知 // 訂閱0號數據庫任何事件的變化 192.168.61.140:6379> psubscribe __keyevent@0__:* Reading messages... (press Ctrl-C to quit) 1) "psubscribe" 2) "__keyevent@0__:*" 3) (integer) 1 // 出現以上信息,說明訂閱成功 // 當其他客戶端執行 set mykey 123456 時,該訂閱可收到以下信息 1) "pmessage" 2) "__keyevent@0__:*" 3) "__keyevent@0__:set" 4) "mykey"
以上收到的訂閱信息中事件是主體,其信息可以概括為:“0號數據庫發生了set事件,發生這個事件的key信息為mykey”。
3-4-3、模式訂閱的配置
a、配置和通配符
要使用psubscribe命令進行鍵事件的訂閱,就首先需要在Redis的主配置文件中對模式訂閱進行設定。注意,如果您只是使用psubscribe命令通過Channel發送消息到訂閱者,或者更單純的使用publish命令和subscribe命令組合通過Channel發送和接收消息,就不需要進行這樣的配置。
默認情況下Redis服務下的鍵空間通知和鍵事件通知都是關閉的。在redis.conf文件下,有專門的“EVENT NOTIFICATION”區域進行設定,設置的格式為:
...... notify-keyspace-events [通配符] ......
通配符的定義描述如下:
- K:啟用keyspace鍵空間通知,客戶端可以使用__keyspace@<db>__為前綴的格式使用訂閱功能。
- E:啟用keyevent鍵事件通知,客戶端可以使用__keyevent@<db>__為前綴的格式使用訂閱功能。
- g:監控一般性事件,包括但不限於對del,expire,rename事件的監控。
- $:啟用對字符串格式(即一般K-V結構)命令的監控。
- l:啟用對List數據結構命令的監控。
- s:啟用對Set數據結構命令的監控。
- h:啟用對Hash數據結構命令的監控。
- z:啟用對ZSet數據結構命令的監控。
- x:啟用對過期事件的監控。
- e:啟用對驅逐事件的監控,當某個鍵因maxmemory達到設置時,使用策略進行內存清理,會產生這個事件。
- A:g$lshzxe通配符組合的別名,也就是說”AKE”這樣的通配符組合,意味着所有事件。
以下的幾個實例說明了配置格式中通配符的用法:
// 監控任何數據格式的所有事件,包括鍵空間通知和鍵事件通知 notify-keyspace-events "AKE" // 只監控字符串結構的所有事件,包括鍵空間通知和鍵事件通知 notify-keyspace-events "g$KExe" // 只監控所有鍵事件通知 notify-keyspace-events "AE" // 只監控Hash數據解構的鍵空間通知 notify-keyspace-events "ghKxe" // 只監控Set數據結構的鍵事件通知 notify-keyspace-events "gsExe"
注意,在Redis主配置文件中進行事件通知的配置,其配置效果是全局化的。也就是說所有連接到Redis服務的客戶端都會使用這樣的Key事件通知邏輯。但如果單獨需要為某一個客戶端會話設置獨立的Key事件通知邏輯,則可以在客戶端成功連接Redis服務后,使用類似如下的命令進行設置:
...... 192.168.61.140:6379> config set notify-keyspace-events KEA OK
b、鍵事件訂閱
完成鍵事件的配置后,就可以使用psubscribe命令在客戶端訂閱消息通知了。這個過程還是需要使用通配符參數,才能完成訂閱指定。通配符格式如下所示:
psubscribe __[keyspace|keyevent]@<db>__:[prefix] // 例如: // 訂閱0號數據庫中,所有的鍵變化事件,進行鍵空間通知 psubscribe __keyspace@0__:* // 訂閱0號數據庫,所有的鍵變化事件,進行鍵空間通知和鍵事件通知 psubscribe __key*@0__:*
注意,就如上文所提到的那樣,客戶端能夠進行鍵信息變化事件訂閱的前提是Redis服務端或者這個客戶端會話本身開啟了相應配置。以下舉例說明psubscribe命令中參數的使用方式:
// 注意,Redis服務上的配置信息如下 // notify-keyspace-events "gsExe" // 即是說只允許監控Set結構的所有事件,並且之啟用了鍵事件通知,沒有啟用鍵空間通知。 // 客戶端使用以下命令開始訂閱Key的變化事件 192.168.61.140:6379> psubscribe __key*@0__:* // 以上命令訂閱了0號數據庫所有鍵信息的變化通知,包括鍵事件通知和鍵空間通知 Reading messages... (press Ctrl-C to quit) 1) "psubscribe" 2) "__key*@0__:*" 3) (integer) 1 // 接着,已連接到Redis服務上的另一個客戶端執行了如下命令 // > sadd mysetkey rt // 那么收到的消息通知為 1) "pmessage" 2) "__key*@0__:*" 3) "__keyevent@0__:sadd" 4) "mysetkey"
以上實例操作中有兩個問題需要單獨進行說明:
-
當客戶端使用psubscribe命令進行訂閱時(psubscribe __key*@0__:*),實際上是連同keyspace(鍵空間通知)和keyevent(鍵事件通知)一起訂閱了。那么按照上文介紹的內容來說,這個訂閱者本該收到兩條事件消息。一條消息的描述重點在key上,另一條消息的描述重點在sadd事件上。但實際情況是,這個訂閱者只收到了以描述重點在事件上的鍵事件通知。這是因為在以上實例中特別說明的一點:Redis服務端只開啟鍵事件通知的配置。所以無論客戶端如何訂閱鍵空間通知,也收不到任何消息。
-
另外,包括Redis官方資料在內的資料都在闡述這樣一個事實,既是通過sadd命令對一個Set結構中的元素進行變更和直接通過“PUBLISH __keyevent@0__:sadd mysetkey”這樣的命令向訂閱者發送消息,在消息訂閱者看來效果都是一樣。但是這兩種不同的操作過程對於Redis存儲的Key數據,則是完全不一樣的。前者的操作方式會改變Redis中存儲的數據狀況,但后者則不會。
3-4-4、Redis訂閱/發布功能的不足
Redis提供的訂閱/發布功能並不完美,更不能和ActiveMQ/RabbitMQ提供的訂閱/發布功能相提並論。
-
首先這些消息並沒有持久化機制,屬於即發即棄模式。也就是說它們不能像ActiveMQ中的消息那樣保證持久化消息訂閱者不會錯過任何消息,無論這些消息訂閱者是否隨時在線。
-
由於本來就是即發即棄的消息模式,所以Redis也不需要專門制定消息的備份和恢復機制。
-
也是由於即發即棄的消息模式,所以Redis也沒有必要專門對使用訂閱/發布功能的客戶端連接進行識別,用來明確該客戶端連接的ID是否在之前已經連接過Redis服務了。ActiveMQ中保持持續通知的功能的前提,就是能夠識別客戶端連接ID的歷史連接情況,以便確定哪些訂閱消息這個客戶端還沒有處理。
-
Redis當前版本有一個簡單的事務機制,這個事務機制可以用於PUBLISH命令。但是完全沒有ActiveMQ中對事務機制和ACK機制那么強的支持。而在我寫作的“系統間通訊”專題中,專門講到了ActiveMQ的ACK機制和事務機制。
-
Redis也沒有為發布者和訂閱者准備保證消息性能的任何方案,例如在大量消息同時到達Redis服務是,如果消息訂閱者來不及完成消費,就可能導致消息堆積。而ActiveMQ中有專門針對這種情況的慢消息機制。
3-5、Redis持久化存儲
從嚴格意義上說,Redis服務提供四種持久化存儲方案:RDB、AOF、虛擬內存(VM)和DISKSTORE。虛擬內存(VM)方式,從Redis Version 2.4開始就被官方明確表示不再建議使用,Version 3.2版本中更找不到關於虛擬內存(VM)的任何配置范例,Redis的主要作者Salvatore Sanfilippo還專門寫了一篇論文,來反思Redis對虛擬內存(VM)存儲技術的支持問題。
至於DISKSTORE方式,是從Redis Version 2.8版本開始提出的一個存儲設想,到目前為止Redis官方也沒有在任何stable版本中明確建議使用這用方式。在Version 3.2版本中同樣找不到對於這種存儲方式的明確支持。從網絡上能夠收集到的各種資料來看,DISKSTORE方式和RDB方式還有這一些千絲萬縷的聯系,不過各位讀者也知道,除了官方文檔以外網絡資料很多就是大抄。
最關鍵的是目前官方文檔上能夠看到的Redis對持久化存儲的支持明確的就只有兩種方案(https://redis.io/topics/persistence):RDB和AOF。所以本文也只會具體介紹這兩種持久化存儲方案的工作特定和配置要點。
3-5-1、RDB
RDB中文名為快照/內存快照,它的過程很好理解,就是Redis按照一定的時間周期將目前服務中的所有數據全部寫入到磁盤中。但這個過程說起簡單,實際上呢有很多細節需要被處理。Redis主配置文件的“REPLICATION”部分,放置了對這個過程的配置選項。在我們后續文章中講解Redis支持的主從復制時,也可以看到RDB的影子。
上圖反映了內存快照的大致過程,由於生產環境中我們為Redis開辟的內存區域都比較大(例如6GB),那么將內存中的數據同步到硬盤的過程可能就會持續比較長的時間,而實際情況是這段時間Redis服務一般都會收到數據寫操作請求。那么如何保證數據一致性呢?RDB中的核心思路是Copy-on-Write,來保證在進行快照操作的這段時間,需要壓縮寫入磁盤上的數據在內存中不會發生變化。在正常的快照操作中,一方面Redis主進程會fork一個新的快照進程專門來做這個事情,這樣保證了Redis服務不會停止對客戶端包括寫請求在內的任何響應。另一方面這段時間發生的數據變化會以副本的方式存放在另一個新的內存區域,待快照操作結束后才會同步到原來的內存區域。
另一個問題是,在進行快照操作的這段時間,如果發生服務崩潰怎么辦?很簡單,在沒有將數據全部寫入到磁盤前,這次快照操作都不算成功。如果出現了服務崩潰的情況,將以上一次完整的RDB快照文件作為恢復內存數據的參考。也就是說,在快照操作過程中不能影響上一次的備份數據。Redis服務會在磁盤上創建一個臨時文件進行數據操作,待操作成功后才會用這個臨時文件替換掉上一次的備份。
以下是Redis中關於內存快照的主要配置信息:
- 快照周期:內存快照雖然可以通過技術人員手動執行SAVE或BGSAVE命令來進行,但生產環境下多數情況都會設置其周期性執行條件。Redis中默認的周期新設置如下:
# 周期性執行條件的設置格式為 save <seconds> <changes> # 默認的設置為: save 900 1 save 300 10 save 60 10000 # 以下設置方式為關閉RDB快照功能 save ""
以上三項默認信息設置代表的意義是:如果900秒內有1條Key信息發生變化,則進行快照;如果300秒內有10條Key信息發生變化,則進行快照;如果60秒內有10000條Key信息發生變化,則進行快照。讀者可以按照這個規則,根據自己的實際請求壓力進行設置調整。
-
stop-writes-on-bgsave-error:上文提到的在快照進行過程中,主進程照樣可以接受客戶端的任何寫操作的特性,是指在快照操作正常的情況下。如果快照操作出現異常(例如操作系統用戶權限不夠、磁盤空間寫滿等等)時,Redis就會禁止寫操作。這個特性的主要目的是使運維人員在第一時間就發現Redis的運行錯誤,並進行解決。一些特定的場景下,您可能需要對這個特性進行配置,這時就可以調整這個參數項。該參數項默認情況下值為yes,如果要關閉這個特性,指定即使出現快照錯誤Redis一樣允許寫操作,則可以將該值更改為no。
-
rdbcompression:該屬性將在字符串類型的數據被快照到磁盤文件時,啟用LZF壓縮算法。Redis官方的建議是請保持該選項設置為yes,因為“it’s almost always a win”。
-
rdbchecksum:從RDB快照功能的version 5 版本開始,一個64位的CRC冗余校驗編碼會被放置在RDB文件的末尾,以便對整個RDB文件的完整性進行驗證。這個功能大概會多損失10%左右的性能,但獲得了更高的數據可靠性。所以如果您的Redis服務需要追求極致的性能,就可以將這個選項設置為no。
-
dbfilename:RDB文件在磁盤上的名稱。
-
dir:RDB文件的存儲路徑。默認設置為“./”,也就是Redis服務的主目錄。
3-5-2、AOF
由於是周期性的同步,所以RDB存在的最大問題就是在Redis異常崩潰,需要從最近一次RDB文件恢復數據時,常常出現最近一批更新的數據丟失,而且根據快照的周期設置,這批數據的總量還可能比較大。另外,雖然使用專門的快照進程進行快照數據同步的方式,本身不會造成Redis服務出現卡頓。但如果需要快照的數據量特別大,操作系統基本上會將CPU資源用到快照操作上去,這可能間接造成包括Redis主進程在內的其它進程被掛起。所以,以一個較大的時間周期全部同步Redis數據狀態的快照方式,在非常高並發的情況下並不是最好的解決方法。
雖然RDB快照基本上可以應付我們遇到的大多數業務場景,也可以滿足至少80%業務系統設計時的預想性能壓力,但為了盡可能解決RDB的工作缺陷,Redis還是提供了另一種數據持久化方式——AOF。AOF全稱是Append Only File,從字面上理解就是“只進行增加的文件”。在本專題中,我們在介紹InnoDB的工作過程時,也介紹了類似“只進行增加的文件”,就是InnoDB中最關鍵的重做日志。
在物理磁盤的操作無論是機械磁盤還是固態磁盤,使用順序讀寫都將獲得比隨機讀寫好得多的I/O性能。所以我們可以看到無論是關系型數據庫、NoSQL數據庫還是之前的業務案例,無一例外都追求在物理磁盤上盡可能進行順序讀寫操作。
AOF方式的核心設計思量可以總結為一句話:忠實記錄Redis服務啟動成功后的每一次影響數據狀態的操作命令,以便在Redis服務異常崩潰的情況出現時,可以按照這些操作命令恢復數據狀態。既然要記錄每次影響數據狀態的操作命令,就意味着AOF文件會越來越大!這是必然的。還好Redis為AOF提供了一種重寫AOF文件的功能,保證了AOF文件中可以存儲盡可能少的操作命令就能保證數據恢復到最新狀態,這個功能被稱為日志重寫功能(請注意這可不是我們在講解InnoDB時提到的重做日志)。
舉個例子,操作人員在Redis中設置了一個K-V結構:mykey3 = yinwenjie,之后有刪除了這個Key信息。那么AOF文件中記錄的動作可能如下所示(AOF文件中的內容可以直接通過各種文本編輯工具直接查看):
# cat ./appendonly.aof
...... *3 $3 SET $6 mykey3 $12 yinwenjie111 ...... *2 $3 del $6 mykey3 ......
可以看到以上AOF文件中的內容,如實記錄了這兩個操作:設置Key和刪除Key。但是這種日志記錄過程對恢復Key的信息沒有任何幫助,因為“mykey3”這個Key信息注定在最新的Redis內存中是不存在的。所以一旦我們運行“重寫日志”命令(可以是設定的條件也可以直接運行“BGREWRITEAOF”命令),那么整理后的AOF文件的內容可能就是如下所示了:
# cat ./appendonly.aof ...... ......
在AOF文件中對mykey3這個Key信息的操作過程記錄消失了!這不但縮小了AOF文件還沒有對數據恢復過程造成任何困擾。Redis主配置文件中關於AOF功能的設定可以在“APPEND ONLY MODE”部分找到:
-
appendonly:默認情況下AOF功能是關閉的,將該選項改為yes以便打開Redis的AOF功能。
-
appendfilename:這個參數項很好理解了,就是AOF文件的名字。
-
appendfsync:這個參數項是AOF功能最重要的設置項之一,主要用於設置“正真執行”操作命令向AOF文件中同步的策略。什么叫“正真執行”呢?還記得我們在本專題中介紹的Linux操作系統對磁盤設備的操作方式嗎? 為了保證操作系統中I/O隊列的操作效率,應用程序提交的I/O操作請求一般是被放置在Linux Page Cache中的,然后再由Linux操作系統中的策略自行決定正在寫到磁盤上的時機。而Redis中有一個fsync()函數,可以將Page Cache中待寫的數據真正寫入到物理設備上,而缺點是頻繁調用這個fsync()函數干預操作系統的既定策略,可能導致I/O卡頓的現象頻繁 。如果您想繼續了解操作系統上的工作的塊存儲技術,可以參看筆者另外幾篇文章《架構設計:系統存儲(4)——塊存儲方案(4)》
appendfsync參數項可以設置三個值,分別是:always、everysec、no,默認的值為everysec。
always參數值,會使得AOF對數據的保存非常穩健。其設置意義是只要有一個寫操作命令執行成功,就執行一次fsync函數調用。所以很顯然always的設定值,就是三個選項值中處理效率最慢的。
no參數值,這個設置值表示Redis不會將執行成功的操作命令正真刷入AOF文件,而是完成操作系統級別的寫操作后就認為AOF文件記錄成功了,后續的I/O操作完全依賴於操作系統的設定,一般30秒會刷一次。
everysec參數值,這是默認的設置值,也是可以在數據穩健性和性能上平衡較好策略。它表示每秒鍾都做一次fsync函數調用,正真做AOF文件的寫入操作。
-
no-appendfsync-on-rewrite:always和everysec的設置會使正真的I/O操作高頻度的出現,甚至會出現長時間的卡頓情況,這個問題出現在操作系統層面上,所有靠工作在操作系統之上的Redis是沒法解決的。為了盡量緩解這個情況,Redis提供了這個設置項,保證在完成fsync函數調用時,不會將這段時間內發生的命令操作放入操作系統的Page Cache(這段時間Redis還在接受客戶端的各種寫操作命令)。
-
auto-aof-rewrite-percentage:上文說到在生產環境下,技術人員不可能隨時隨地使用“BGREWRITEAOF”命令去重寫AOF文件。所以更多時候我們需要依靠Redis中對AOF文件的自動重寫策略。Redis中對觸發自動重寫AOF文件的操作提供了兩個設置:auto-aof-rewrite-percentage表示如果當前AOF文件的大小超過了上次重寫后AOF文件的百分之多少后,就再次開始重寫AOF文件。例如該參數值的默認設置值為100,意思就是如果AOF文件的大小超過上次AOF文件重寫后的1倍,就啟動重寫操作。
-
auto-aof-rewrite-min-size:參考auto-aof-rewrite-percentage選項的介紹,auto-aof-rewrite-min-size設置項表示啟動AOF文件重寫操作的AOF文件最小大小。如果AOF文件大小低於這個值,則不會觸發重寫操作。注意,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size只是用來控制Redis中自動對AOF文件進行重寫的情況,如果是技術人員手動調用“BGREWRITEAOF”命令,則不受這兩個限制條件左右。
3-5-3、持久化存儲的性能建議
關於持久化存儲的性能建議,我們將結合后文介紹的Redis集群方案一起進行分析