安全性
設置客戶端連接后進行任何其他指令前需要使用的密碼。
警告:因為redis 速度相當快,所以在一臺比較好的服務器下,一個外部的用戶可以在一秒鐘進
行150K 次的密碼嘗試,這意味著你需要指定非常非常強大的密碼來防止暴力破解。
下面我們做一個實驗,說明redis 的安全性是如何實現的。
# requirepass foobared requirepass beijing
我們設置了連接的口令是beijing
那么們啟動一個客戶端試一下:
[root@localhost redis-2.2.12]# src/redis-cli redis 127.0.0.1:6379> keys * (error) ERR operation not permitted redis 127.0.0.1:6379>
說明權限太小,我們可以在當前的這個窗口中設置口令
`redis 127.0.0.1:6379> auth beijing
OK
redis 127.0.0.1:6379> keys *
- "name"
redis 127.0.0.1:6379>`
我們還可以在連接到服務器期間就指定一個口令,如下:
`[root@localhost redis-2.2.12]# src/redis-cli -a beijing
redis 127.0.0.1:6379> keys *
- "name"
redis 127.0.0.1:6379>`
可以看到我們在連接的時候就可以指定一個口令。
主從復制
redis 主從復制配置和使用都非常簡單。通過主從復制可以允許多個slave server 擁有和
master server 相同的數據庫副本。
redis 主從復制特點:
- master 可以擁有多個slave
- 多個slave 可以連接同一個master 外,還可以連接到其他slave
- 主從復制不會阻塞master,在同步數據時,master 可以繼續處理client 請求
- 提高系統的伸縮性
redis 主從復制過程:
當配置好slave 后,slave 與master 建立連接,然后發送sync 命令。無論是第一次連接還是重新連接,master 都會啟動一個后臺進程,將數據庫快照保存到文件中,同時master 主進程會開始收集新的寫命令并緩存。后臺進程完成寫文件后,master 就發送文件給slave,slave將文件保存到硬盤上,再加載到內存中,接著master 就會把緩存的命令轉發給slave,后續master 將收到的寫命令發送給slave。如果master 同時收到多個slave 發來的同步連接命令,master 只會啟動一個進程來寫數據庫鏡像,然后發送給所有的slave。
如何配置
配置slave 服務器很簡單,只需要在slave 的配置文件中加入如下配置
slaveof 192.168.1.1 6379 #指定master 的ip 和端口
下面我們做一個實驗來演示如何搭建一個主從環境:
# slaveof <masterip> <masterport> slaveof localhost 6379
我們在一臺機器上啟動主庫(端口6379),從庫(端口6378)
啟動后主庫控制臺日志如下:
[root@localhost redis-2.2.12]# src/redis-server redis.conf [7064] 09 Aug 20:13:12 * Server started, Redis version 2.2.12 [7064] 09 Aug 20:13:12 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. [7064] 09 Aug 20:13:12 * The server is now ready to accept connections on port 6379 [7064] 09 Aug 20:13:13 - 0 clients connected (0 slaves), 539512 bytes in use [7064] 09 Aug 20:13:18 - 0 clients connected (0 slaves), 539512 bytes in use [7064] 09 Aug 20:13:20 - Accepted 127.0.0.1:37789 [7064] 09 Aug 20:13:20 * Slave ask for synchronization [7064] 09 Aug 20:13:20 * Starting BGSAVE for SYNC [7064] 09 Aug 20:13:20 * Background saving started by pid 7067 [7067] 09 Aug 20:13:20 * DB saved on disk [7064] 09 Aug 20:13:20 * Background saving terminated with success [7064] 09 Aug 20:13:20 * Synchronization with slave succeeded [7064] 09 Aug 20:13:23 - 0 clients connected (1 slaves), 547380 bytes in use
啟動后從庫控制臺日志如下:
[root@localhost redis-2.2.12]# src/redis-server redis.slave [7066] 09 Aug 20:13:20 * Server started, Redis version 2.2.12 [7066] 09 Aug 20:13:20 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. [7066] 09 Aug 20:13:20 * The server is now ready to accept connections on port 6378 [7066] 09 Aug 20:13:20 - 0 clients connected (0 slaves), 539548 bytes in use [7066] 09 Aug 20:13:20 * Connecting to MASTER... [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync started: SYNC sent [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync: receiving 10 bytes from master [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync: Loading DB in memory [7066] 09 Aug 20:13:20 * MASTER <-> SLAVE sync: Finished with success [7068] 09 Aug 20:13:20 * SYNC append only file rewrite performed [7066] 09 Aug 20:13:20 * Background append only file rewriting started by pid 7068 [7066] 09 Aug 20:13:21 * Background append only file rewriting terminated with success [7066] 09 Aug 20:13:21 * Parent diff flushed into the new append log file with success (0 bytes) [7066] 09 Aug 20:13:21 * Append only file successfully rewritten. [7066] 09 Aug 20:13:21 * The new append only file was selected for future appends. [7066] 09 Aug 20:13:25 - 1 clients connected (0 slaves), 547396 bytes in use
我們在主庫上設置一對鍵值對
redis 127.0.0.1:6379> set name HongWan OK redis 127.0.0.1:6379>
在從庫上取一下這個鍵
redis 127.0.0.1:6378> get name "HongWan" redis 127.0.0.1:6378>
說明主從是同步正常的.
那么我們如何判斷哪個是主哪個是從呢?我們只需調用info 這個命令就可以得到主從的信息
了,我們在從庫上執行info 命令
redis 127.0.0.1:6378> info . . . role:slave master_host:localhost master_port:6379 master_link_status:up master_last_io_seconds_ago:10 master_sync_in_progress:0 db0:keys=1,expires=0 redis 127.0.0.1:6378>
里面有一個角色標識,來判斷是主庫還是從庫,對于本例是一個從庫,同時還有一個master_link_status 用于標明主從是否異步,如果此值=up,說明同步正常;如果此值=down,
說明同步異步;
db0:keys=1,expires=0, 用于說明數據庫有幾個key,以及過期key 的數量。
事務控制
redis 對事務的支持目前還比較簡單。redis 只能保證一個client 發起的事務中的命令可以連續的執行,而中間不會插入其他client 的命令。由于redis 是單線程來處理所有client 的請求的所以做到這點是很容易的。一般情況下redis 在接受到一個client 發來的命令后會立即處理并返回處理結果,但是當一個client 在一個連接中發出multi 命令,這個連接會進入一個事務上下文,該連接后續的命令并不是立即執行,而是先放到一個隊列中。當從此連接受到exec 命令后,redis 會順序的執行隊列中的所有命令。并將所有命令的運行結果打包到一起返回給client.然后此連接就結束事務上下文。
簡單事務控制
下面可以看一個例子
`redis 127.0.0.1:6379> get age
"33"
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> set age 10
QUEUED
redis 127.0.0.1:6379> set age 20
QUEUED
redis 127.0.0.1:6379> exec
- OK
- OK
redis 127.0.0.1:6379> get age
"20"
redis 127.0.0.1:6379>`
從這個例子我們可以看到2 個set age 命令發出后并沒執行而是被放到了隊列中。調用exec后2 個命令才被連續的執行,最后返回的是兩條命令執行后的結果。
如何取消一個事務
我們可以調用discard 命令來取消一個事務,讓事務回滾。接著上面例子
redis 127.0.0.1:6379> get age "20" redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379> set age 30 QUEUED redis 127.0.0.1:6379> set age 40 QUEUED redis 127.0.0.1:6379> discard OK redis 127.0.0.1:6379> get age "20" redis 127.0.0.1:6379>
可以發現這次2 個set age 命令都沒被執行。discard 命令其實就是清空事務的命令隊列并退出事務上下文,也就是我們常說的事務回滾。
樂觀鎖復雜事務控制
在本小節開始前,我們有必要向讀者朋友簡單介紹一下樂觀鎖的概念,并舉例說明樂觀鎖是怎么工作的。
樂觀鎖:大多數是基于數據版本(version)的記錄機制實現的。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表添加一個“version”字段來實現讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加1。此時,將提交數據的版本號與數據庫表對應記錄的當前版本號進行比對,如果提交的數據版本號大于數據庫表當前版本號,則予以更新,否則認為是過期數據。
樂觀鎖實例:假設數據庫中帳戶信息表中有一個version 字段,當前值為1;而當前帳戶余額字段(balance)為$100。下面我們將用時序表的方式來為大家演示樂觀鎖的實現原理:
操作員A | 操作員B |
---|---|
(1)、操作員A 此時將用戶信息讀出(此時version=1),并準備從其帳戶余額中扣除$50($100-$50) | (2)、在操作員A 操作的過程中,操作員B 也讀入此用戶信息(此時version=1),并準備從其帳戶余額中扣除$20($100-$20) |
(3)、操作員A 完成了修改工作,將數據版本號加1(此時version=2),連同帳戶扣除后余額(balance=$50),提交至數據庫更新,此時由于提交數據版本大于數據庫記錄當前版本,數據被更新,數據庫記錄version更新為2 | |
(4)、操作員B 完成了操作,也將版本號加1( version=2 ) 并試圖向數據庫提交數據(balance=$80),但此時比對數據庫記錄版本時發現,操作員B 提交的數據版本號為2,數據庫記錄當前版本也為2,不滿足“提交版本必須大于記錄當前版本才能執行更新”的樂觀鎖策略,因此,操作員B 的提交被駁回 |
這樣,就避免了操作員B 用基于version=1 的舊數據修改的結果來覆蓋操作員A 的操作結果
的可能。
即然樂觀鎖比悲觀鎖要好很多,redis 是否也支持呢?答案是支持, redis 從2.1.0 開始就支持樂觀鎖了,可以顯式的使用watch 對某個key 進行加鎖,避免悲觀鎖帶來的一系列問題。Redis 樂觀鎖實例:假設有一個age 的key,我們開2 個session 來對age 進行賦值操作,我們來看一下結果如何。
Session 1 | Session 2 |
---|---|
(1)第1 步redis 127.0.0.1:6379> get age "10" redis 127.0.0.1:6379> watch age OK redis 127.0.0.1:6379> multi OK redis 127.0.0.1:6379> | |
(2)第2 步 redis 127.0.0.1:6379> set age 30 OK redis 127.0.0.1:6379> get age "30" redis 127.0.0.1:6379> | |
(3)第3 步 redis 127.0.0.1:6379> set age 20 QUEUED redis 127.0.0.1:6379> exec (nil) redis 127.0.0.1:6379> get age "30" redis 127.0.0.1:6379> |
從以上實例可以看到在
第一步,Session 1 還沒有來得及對age 的值進行修改
第二步,Session 2 已經將age 的值設為30
第三步,Session 1 希望將age 的值設為20,但結果一執行返回是nil,說明執行失敗,之后我們再取一下age 的值是30,這是由于Session 1 中對age 加了樂觀鎖導致的。
watch 命令會監視給定的key,當exec 時候如果監視的key 從調用watch 后發生過變化,則整個事務會失敗。也可以調用watch 多次監視多個key.這樣就可以對指定的key 加樂觀鎖了。注意watch 的key 是對整個連接有效的,事務也一樣。如果連接斷開,監視和事務都會被自動清除。當然了exec,discard,unwatch 命令都會清除連接中的所有監視。
redis 的事務實現是如此簡單,當然會存在一些問題。第一個問題是redis 只能保證事務的每個命令連續執行,但是如果事務中的一個命令失敗了,并不回滾其他命令,比如使用的命令類型不匹配。下面將以一個實例的例子來說明這個問題:
`redis 127.0.0.1:6379> get age
"30"
redis 127.0.0.1:6379> get name
"HongWan"
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> incr age
QUEUED
redis 127.0.0.1:6379> incr name
QUEUED
redis 127.0.0.1:6379> exec
- (integer) 31
- (error) ERR value is not an integer or out of range
redis 127.0.0.1:6379> get age
"31"
redis 127.0.0.1:6379> get name
"HongWan"
redis 127.0.0.1:6379>`
從這個例子中可以看到,age 由于是個數字,那么它可以有自增運算,但是name 是個字符串,無法對其進行自增運算,所以會報錯,如果按傳統關系型數據庫的思路來講,整個事務都會回滾,但是我們看到redis 卻是將可以執行的命令提交了,所以這個現象對于習慣于關系型數據庫操作的朋友來說是很別扭的,這一點也是redis 今天需要改進的地方。
持久化機制
redis 是一個支持持久化的內存數據庫,也就是說redis 需要經常將內存中的數據同步到磁盤來保證持久化。redis 支持兩種持久化方式,一種是Snapshotting(快照)也是默認方式,另一種是Append-only file(縮寫aof)的方式。下面分別介紹:
snapshotting 方式
快照是默認的持久化方式。這種方式是就是將內存中數據以快照的方式寫入到二進制文件中,默認的文件名為dump.rdb。可以通過配置設置自動做快照持久化的方式。我們可以配置redis在n 秒內如果超過m 個key 被修改就自動做快照,下面是默認的快照保存配置
save 900 1 #900 秒內如果超過1 個key 被修改,則發起快照保存
save 300 10 #300 秒內容如超過10 個key 被修改,則發起快照保存
save 60 10000
下面介紹詳細的快照保存過程:
- redis 調用fork,現在有了子進程和父進程。
- 父進程繼續處理client 請求,子進程負責將內存內容寫入到臨時文件。由于os 的實時復制機制(copy on write)父子進程會共享相同的物理頁面,當父進程處理寫請求時os 會為父進程要修改的頁面創建副本,而不是寫共享的頁面。所以子進程地址空間內的數據是fork時刻整個數據庫的一個快照。
- 當子進程將快照寫入臨時文件完畢后,用臨時文件替換原來的快照文件,然后子進程退出。
client 也可以使用save 或者bgsave 命令通知redis 做一次快照持久化。save 操作是在主線程中保存快照的,由于redis 是用一個主線程來處理所有client 的請求,這種方式會阻塞所有client 請求。所以不推薦使用。另一點需要注意的是,每次快照持久化都是將內存數據完整寫入到磁盤一次,并不是增量的只同步變更數據。如果數據量大的話,而且寫操作比較多,必然會引起大量的磁盤io操作,可能會嚴重影響性能。
下面將演示各種場景的數據庫持久化情況
redis 127.0.0.1:6379> set name HongWan OK redis 127.0.0.1:6379> get name "HongWan" redis 127.0.0.1:6379> shutdown redis 127.0.0.1:6379> quit
我們先設置了一個name 的鍵值對,然后正常關閉了數據庫實例,數據是否被保存到磁盤了
呢?我們來看一下服務器端是否有消息被記錄下來了:
[6563] 09 Aug 18:58:58 * The server is now ready to accept connections on port 6379 [6563] 09 Aug 18:58:58 - 0 clients connected (0 slaves), 539540 bytes in use [6563] 09 Aug 18:59:02 - Accepted 127.0.0.1:58005 [6563] 09 Aug 18:59:03 - 1 clients connected (0 slaves), 547368 bytes in use [6563] 09 Aug 18:59:08 - 1 clients connected (0 slaves), 547424 bytes in use [6563] 09 Aug 18:59:12 # User requested shutdown... [6563] 09 Aug 18:59:12 * Saving the final RDB snapshot before exiting. [6563] 09 Aug 18:59:12 * DB saved on disk [6563] 09 Aug 18:59:12 # Redis is now ready to exit, bye bye... [root@localhost redis-2.2.12]#
從日志可以看出,數據庫做了一個存盤的操作,將內存的數據寫入磁盤了。正常的話,磁盤
上會產生一個dump 文件,用于保存數據庫快照,我們來驗證一下:
[root@localhost redis-2.2.12]# ll 總計 188 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog drwxrwxr-x 2 root root 4096 2011-07-22 client-libraries -rw-rw-r-- 1 root root 671 2011-07-22 CONTRIBUTING -rw-rw-r-- 1 root root 1487 2011-07-22 COPYING drwxrwxr-x 4 root root 4096 2011-07-22 deps drwxrwxr-x 2 root root 4096 2011-07-22 design-documents drwxrwxr-x 2 root root 12288 2011-07-22 doc -rw-r--r-- 1 root root 26 08-09 18:59 dump.rdb -rw-rw-r-- 1 root root 652 2011-07-22 INSTALL -rw-rw-r-- 1 root root 337 2011-07-22 Makefile -rw-rw-r-- 1 root root 1954 2011-07-22 README -rw-rw-r-- 1 root root 19067 08-09 18:48 redis.conf drwxrwxr-x 2 root root 4096 08-05 19:12 src drwxrwxr-x 7 root root 4096 2011-07-22 tests -rw-rw-r-- 1 root root 158 2011-07-22 TODO drwxrwxr-x 2 root root 4096 2011-07-22 utils [root@localhost redis-2.2.12]#
硬盤上已經產生了一個數據庫快照了。這時侯我們再將redis 啟動,看鍵值還是否真的持久
化到硬盤了。
`redis 127.0.0.1:6379> keys *
- "name"
redis 127.0.0.1:6379> get name
"HongWan"
redis 127.0.0.1:6379>`
數據被完全持久化到硬盤了。
aof 方式
另外由于快照方式是在一定間隔時間做一次的,所以如果redis 意外down 掉的話,就會丟失最后一次快照后的所有修改。如果應用要求不能丟失任何修改的話,可以采用aof 持久化方式。
下面介紹Append-only file:
aof 比快照方式有更好的持久化性,是由于在使用aof 持久化方式時,redis 會將每一個收到
的寫命令都通過write 函數追加到文件中(默認是appendonly.aof)。當redis 重啟時會通過重
新執行文件中保存的寫命令來在內存中重建整個數據庫的內容。當然由于os 會在內核中緩
存 write 做的修改,所以可能不是立即寫到磁盤上。這樣aof 方式的持久化也還是有可能會
丟失部分修改。不過我們可以通過配置文件告訴redis 我們想要通過fsync 函數強制os 寫入
到磁盤的時機。
有三種方式如下(默認是:每秒fsync 一次)
appendonly yes //啟用aof 持久化方式
appendfsync always //收到寫命令就立即寫入磁盤,最慢,但是保證完全的持久化
appendfsync everysec //每秒鐘寫入磁盤一次,在性能和持久化方面做了很好的折中
appendfsync no //完全依賴os,性能最好,持久化沒保證
接下來我們以實例說明用法:
`redis 127.0.0.1:6379> set name HongWan
OK
redis 127.0.0.1:6379> set age 20
OK
redis 127.0.0.1:6379> keys *
- "age"
- "name"
redis 127.0.0.1:6379> shutdown
redis 127.0.0.1:6379>`
我們先設置2 個鍵值對,然后我們看一下系統中有沒有產生appendonly.aof 文件
[root@localhost redis-2.2.12]# ll 總計 184 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-r--r-- 1 root root 0 08-09 19:37 appendonly.aof -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog drwxrwxr-x 2 root root 4096 2011-07-22 client-libraries -rw-rw-r-- 1 root root 671 2011-07-22 CONTRIBUTING -rw-rw-r-- 1 root root 1487 2011-07-22 COPYING drwxrwxr-x 4 root root 4096 2011-07-22 deps drwxrwxr-x 2 root root 4096 2011-07-22 design-documents drwxrwxr-x 2 root root 12288 2011-07-22 doc -rw-rw-r-- 1 root root 652 2011-07-22 INSTALL -rw-rw-r-- 1 root root 337 2011-07-22 Makefile -rw-rw-r-- 1 root root 1954 2011-07-22 README -rw-rw-r-- 1 root root 19071 08-09 19:24 redis.conf drwxrwxr-x 2 root root 4096 08-05 19:12 src drwxrwxr-x 7 root root 4096 2011-07-22 tests -rw-rw-r-- 1 root root 158 2011-07-22 TODO drwxrwxr-x 2 root root 4096 2011-07-22 utils [root@localhost redis-2.2.12]#
結果證明產生了,接著我們將redis 再次啟動后來看一下數據是否還在
`[root@localhost redis-2.2.12]# src/redis-cli
redis 127.0.0.1:6379> keys *
- "age"
- "name"
redis 127.0.0.1:6379>`
數據還存在系統中,說明系統是在啟動時執行了一下從磁盤到內存的load 數據的過程。
aof 的方式也同時帶來了另一個問題。持久化文件會變的越來越大。例如我們調用incr test命令100 次,文件中必須保存全部的100 條命令,其實有99 條都是多余的。因為要恢復數據庫的狀態其實文件中保存一條set test 100 就夠了。為了壓縮aof 的持久化文件。redis 提供了bgrewriteaof 命令。收到此命令redis 將使用與快照類似的方式將內存中的數據以命令的方式保存到臨時文件中,最后替換原來的文件。
具體過程如下
1、redis 調用fork ,現在有父子兩個進程
2、子進程根據內存中的數據庫快照,往臨時文件中寫入重建數據庫狀態的命令
3、父進程繼續處理client 請求,除了把寫命令寫入到原來的aof 文件中。同時把收到的寫命令緩存起來。這樣就能保證如果子進程重寫失敗的話并不會出問題。
4、當子進程把快照內容寫入以命令方式寫到臨時文件中后,子進程發信號通知父進程。然后父進程把緩存的寫命令也寫入到臨時文件。
5、現在父進程可以使用臨時文件替換老的aof 文件,并重命名,后面收到的寫命令也開始往新的aof 文件中追加。
需要注意到是重寫aof 文件的操作,并沒有讀取舊的aof 文件,而是將整個內存中的數據庫內容用命令的方式重寫了一個新的aof 文件,這點和快照有點類似。接來我們看一下實際的例子:
我們先調用5 次incr age 命令:
redis 127.0.0.1:6379> incr age (integer) 21 redis 127.0.0.1:6379> incr age (integer) 22 redis 127.0.0.1:6379> incr age (integer) 23 redis 127.0.0.1:6379> incr age (integer) 24 redis 127.0.0.1:6379> incr age (integer) 25 redis 127.0.0.1:6379>
接下來我們看一下日志文件的大小
[root@localhost redis-2.2.12]# ll 總計 188 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-r--r-- 1 root root 259 08-09 19:43 appendonly.aof -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog
大小為259 個字節,接下來我們調用一下bgrewriteaof 命令將內存中的數據重新刷到磁盤的
日志文件中
redis 127.0.0.1:6379> bgrewriteaof Background append only file rewriting started redis 127.0.0.1:6379>
再看一下磁盤上的日志文件大小
[root@localhost redis-2.2.12]# ll 總計 188 -rw-rw-r-- 1 root root 9602 2011-07-22 00-RELEASENOTES -rw-r--r-- 1 root root 127 08-09 19:45 appendonly.aof -rw-rw-r-- 1 root root 55 2011-07-22 BUGS -rw-rw-r-- 1 root root 84050 2011-07-22 Changelog
日志文件大小變為127 個字節了,說明原來日志中的重復記錄已被刷新掉了。
發布及訂閱消息
發布訂閱(pub/sub)是一種消息通信模式,主要的目的是解耦消息發布者和消息訂閱者之間的耦合,這點和設計模式中的觀察者模式比較相似。pub/sub 不僅僅解決發布者和訂閱者直接代碼級別耦合也解決兩者在物理部署上的耦合。redis 作為一個pub/sub 的server,在訂閱者和發布者之間起到了消息路由的功能。訂閱者可以通過subscribe 和psubscribe 命令向redis server 訂閱自己感興趣的消息類型,redis 將消息類型稱為通道(channel)。當發布者通過publish 命令向redis server 發送特定類型的消息時。訂閱該消息類型的全部client 都會收到此消息。這里消息的傳遞是多對多的。一個client 可以訂閱多個channel,也可以向多個channel發送消息。
下面做個實驗。這里使用3 不同的client, client1 用于訂閱tv1 這個channel 的消息,client2用于訂閱tv1 和tv2 這2 個chanel 的消息,client3 用于發布tv1 和tv2 的消息。
Client 1 | Client 2 | Client 3 |
---|---|---|
redis 127.0.0.1:6379> subscribe tv1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 | redis 127.0.0.1:6379> subscribe tv1 tv2 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "subscribe" 2) "tv2" 3) (integer) 2 | |
redis 127.0.0.1:6379> publish tv1 program1 (integer) 2 redis 127.0.0.1:6379> | ||
redis 127.0.0.1:6379> subscribe tv1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "message" 2) "tv1" 3) "program1" | redis 127.0.0.1:6379> subscribe tv1 tv2 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "subscribe" 2) "tv2" 3) (integer) 2 1) "message" 2) "tv1" 3) "program1" | |
redis 127.0.0.1:6379> publish tv2 program2 (integer) 1 redis 127.0.0.1:6379> | ||
redis 127.0.0.1:6379> subscribe tv1 | redis 127.0.0.1:6379> subscribe tv1 tv2 | |
Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "message" 2) "tv1" 3) "program1" | Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "tv1" 3) (integer) 1 1) "subscribe" 2) "tv2" 3) (integer) 2 1) "message" 2) "tv1" 3) "program1" 1) "message" 2) "tv2" 3) "program2" |
下面將詳細的解釋一下上面的例子
1、client1 訂閱了tv1 這個channel 這個頻道的消息,client2 訂閱了tv1 和tv2 這2 個頻道的消息
2、client3 是用于發布tv1 和tv2 這2 個頻道的消息發布者
3、接下來我們在client3 發布了一條消息”publish tv1 program1”,大家可以看到這條消息是發往tv1 這個頻道的
4、理所當然的client1 和client2 都接收到了這個頻道的消息
5、 然后client3 又發布了一條消息”publish tv2 program2”,這條消息是發往tv2 的,由于client1 并沒有訂閱tv1,所以client1 的結果中并沒有顯示出任何結果,但client2 訂閱了這個頻道,所以client2 是會有返回結果的。
我們也可以用psubscribe tv*的方式批量訂閱以tv 開頭的頻道的內容。
看完這個小例子后應該對pub/sub 功能有了一個感性的認識。需要注意的是當一個連接通過subscribe 或者psubscribe 訂閱通道后就進入訂閱模式。在這種模式除了再訂閱額外的通道或者用unsubscribe 或者punsubscribe 命令退出訂閱模式,就不能再發送其他命令。另外使用psubscribe 命令訂閱多個通配符通道,如果一個消息匹配上了多個通道模式的話,會多次收到同一個消息。
Pipeline 批量發送請求
redis 是一個cs 模式的tcp server,使用和http 類似的請求響應協議。一個client 可以通過一個socket 連接發起多個請求命令。每個請求命令發出后client 通常會阻塞并等待redis 服務處理,redis 處理完后請求命令后會將結果通過響應報文返回給client。基本的通信過程如下:
Client: INCR X Server: 1 Client: INCR X Server: 2 Client: INCR X Server: 3 Client: INCR X Server: 4
基本上四個命令需要8 個tcp 報文才能完成。由于通信會有網絡延遲,假如從client 和server之間的包傳輸時間需要0.125 秒。那么上面的四個命令8 個報文至少會需要1 秒才能完成。這樣即使redis 每秒能處理100 個命令,而我們的client 也只能一秒鐘發出四個命令。這顯示沒有充分利用redis 的處理能力,怎么樣解決這個問題呢? 我們可以利用pipeline 的方式從client 打包多條命令一起發出,不需要等待單條命令的響應返回,而redis 服務端會處理完多條命令后會將多條命令的處理結果打包到一起返回給客戶端。通信過程如下
Client: INCR X Client: INCR X Client: INCR X Client: INCR X Server: 1 Server: 2 Server: 3 Server: 4
假設不會因為tcp 報文過長而被拆分。可能兩個tcp 報文就能完成四條命令,client 可以將四個incr 命令放到一個tcp 報文一起發送,server 則可以將四條命令的處理結果放到一個tcp報文返回。通過pipeline 方式當有大批量的操作時候,我們可以節省很多原來浪費在網絡延遲的時間,需要注意到是用pipeline 方式打包命令發送,redis 必須在處理完所有命令前先緩存起所有命令的處理結果。打包的命令越多,緩存消耗內存也越多。所以并不是打包的命令越多越好。具體多少合適需要根據具體情況測試。下面是個Java 使用pipeline 的實驗:
import org.jredis.JRedis; import org.jredis.connector.ConnectionSpec; import org.jredis.ri.alphazero.JRedisClient; import org.jredis.ri.alphazero.JRedisPipelineService; import org.jredis.ri.alphazero.connection.DefaultConnectionSpec; public class TestPipeline { public static void main(String[] args) { long start = System.currentTimeMillis(); //采用pipeline 方式發送指令 usePipeline(); long end = System.currentTimeMillis(); System.out.println("用pipeline 方式耗時:" + (end - start) + "毫秒"); start = System.currentTimeMillis(); //普通方式發送指令 withoutPipeline(); end = System.currentTimeMillis(); System.out.println("普通方式耗時:" + (end - start) + "毫秒"); } //采用pipeline 方式發送指令 private static void usePipeline() { try { ConnectionSpec spec = DefaultConnectionSpec.newSpec( "192.168.115.170", 6379, 0, null); JRedis jredis = new JRedisPipelineService(spec); for (int i = 0; i < 100000; i++) { jredis.incr("test2"); } jredis.quit(); } catch (Exception e) { } } //普通方式發送指令 private static void withoutPipeline() { try { JRedis jredis = new JRedisClient("192.168.115.170", 6379); for (int i = 0; i < 100000; i++) { jredis.incr("test2"); } jredis.quit(); } catch (Exception e) { } } }
執行結果如下:
-- JREDIS -- INFO: Pipeline thread <response-handler> started. -- JREDIS -- INFO: Pipeline <org.jredis.ri.alphazero.connection.SynchPipelineConnection@1bf73fa> connected 用pipeline 方式耗時:11531 毫秒 -- JREDIS -- INFO: Pipeline <org.jredis.ri.alphazero.connection.SynchPipelineConnection@1bf73fa> disconnected -- JREDIS -- INFO: Pipeline thread <response-handler> stopped. 普通方式耗時:15985 毫秒
所以用兩種方式發送指令,耗時是不一樣的,具體是否使用pipeline 必須要基于大家手中的網絡情況來決定,不能一切都按最新最好的技術來實施,因為它有可能不是最適合你的。
虛擬內存的使用
首先說明下redis 的虛擬內存與操作系統的虛擬內存不是一碼事,但是思路和目的都是相同的。就是暫時把不經常訪問的數據從內存交換到磁盤中,從而騰出寶貴的內存空間用于其他需要訪問的數據。尤其是對于redis 這樣的內存數據庫,內存總是不夠用的。除了可以將數據分割到多個redis server 外。另外的能夠提高數據庫容量的辦法就是使用虛擬內存把那些不經常訪問的數據交換到磁盤上。如果我們的存儲的數據總是有少部分數據被經常訪問,大部分數據很少被訪問,對于網站來說確實總是只有少量用戶經常活躍。當少量數據被經常訪問時,使用虛擬內存不但能提高單臺redis server 數據庫的容量,而且也不會對性能造成太多影響。
redis 沒有使用操作系統提供的虛擬內存機制而是自己在實現了自己的虛擬內存機制,主要的理由有兩點:
- 操作系統的虛擬內存是以4k 頁面為最小單位進行交換的。而redis 的大多數對象都遠小于4k,所以一個操作系統頁面上可能有多個redis 對象。另外redis 的集合對象類型如list,set可能存在于多個操作系統頁面上。最終可能造成只有10%key 被經常訪問,但是所有操作系統頁面都會被操作系統認為是活躍的,這樣只有內存真正耗盡時操作系統才會交換頁面。
2、相比于操作系統的交換方式,redis 可以將被交換到磁盤的對象進行壓縮,保存到磁盤的對象可以去除指針和對象元數據信息,一般壓縮后的對象會比內存中的對象小10 倍,這樣redis的虛擬內存會比操作系統虛擬內存能少做很多io 操作。
下面是vm 相關配置
vm-enabled yes #開啟vm 功能
vm-swap-file /tmp/redis.swap #交換出來的value 保存的文件路徑
vm-max-memory 1000000 #redis 使用的最大內存上限
vm-page-size 32 #每個頁面的大小32 個字節
vm-pages 134217728 #最多使用多少頁面
vm-max-threads 4 #用于執行value 對象換入換出的工作線程數量
redis 的虛擬內存在設計上為了保證key 的查找速度,只會將value 交換到swap 文件中。所以如果是內存問題是由于太多value 很小的key 造成的,那么虛擬內存并不能解決,和操作系統一樣redis 也是按頁面來交換對象的。redis 規定同一個頁面只能保存一個對象。但是一個對象可以保存在多個頁面中。在redis 使用的內存沒超過vm-max-memory 之前是不會交換任何value 的。當超過最大內存限制后,redis 會選擇較過期的對象。如果兩個對象一樣過期會優先交換比較大的對象,精確的公式swappability = age*log(size_in_memory)。對于vm-page-size 的設置應該根據自己的應用將頁面的大小設置為可以容納大多數對象的大小,太大了會浪費磁盤空間,太小了會造成交換文件出現碎片。對于交換文件中的每個頁面,redis
會在內存中對應一個1bit 值來記錄頁面的空閑狀態。所以像上面配置中頁面數量(vm-pages 134217728 )會占用16M 內存用來記錄頁面空閑狀態。vm-max-threads 表示用做交換任務的線程數量。如果大于0 推薦設為服務器的cpu 內核的數量,如果是0 則交換過程在主線程進行。