安全性
設(shè)置客戶端連接后進(jìn)行任何其他指令前需要使用的密碼。
警告:因為redis 速度相當(dāng)快,所以在一臺比較好的服務(wù)器下,一個外部的用戶可以在一秒鐘進(jìn)
行150K 次的密碼嘗試,這意味著你需要指定非常非常強大的密碼來防止暴力破解。
下面我們做一個實驗,說明redis 的安全性是如何實現(xiàn)的。
# requirepass foobared requirepass beijing
我們設(shè)置了連接的口令是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>
說明權(quán)限太小,我們可以在當(dāng)前的這個窗口中設(shè)置口令
`redis 127.0.0.1:6379> auth beijing
OK
redis 127.0.0.1:6379> keys *
- "name"
redis 127.0.0.1:6379>`
我們還可以在連接到服務(wù)器期間就指定一個口令,如下:
`[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>`
可以看到我們在連接的時候就可以指定一個口令。
主從復(fù)制
redis 主從復(fù)制配置和使用都非常簡單。通過主從復(fù)制可以允許多個slave server 擁有和
master server 相同的數(shù)據(jù)庫副本。
redis 主從復(fù)制特點:
- master 可以擁有多個slave
- 多個slave 可以連接同一個master 外,還可以連接到其他slave
- 主從復(fù)制不會阻塞master,在同步數(shù)據(jù)時,master 可以繼續(xù)處理client 請求
- 提高系統(tǒng)的伸縮性
redis 主從復(fù)制過程:
當(dāng)配置好slave 后,slave 與master 建立連接,然后發(fā)送sync 命令。無論是第一次連接還是重新連接,master 都會啟動一個后臺進(jìn)程,將數(shù)據(jù)庫快照保存到文件中,同時master 主進(jìn)程會開始收集新的寫命令并緩存。后臺進(jìn)程完成寫文件后,master 就發(fā)送文件給slave,slave將文件保存到硬盤上,再加載到內(nèi)存中,接著master 就會把緩存的命令轉(zhuǎn)發(fā)給slave,后續(xù)master 將收到的寫命令發(fā)送給slave。如果master 同時收到多個slave 發(fā)來的同步連接命令,master 只會啟動一個進(jìn)程來寫數(shù)據(jù)庫鏡像,然后發(fā)送給所有的slave。
如何配置
配置slave 服務(wù)器很簡單,只需要在slave 的配置文件中加入如下配置
slaveof 192.168.1.1 6379 #指定master 的ip 和端口
下面我們做一個實驗來演示如何搭建一個主從環(huán)境:
# 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
我們在主庫上設(shè)置一對鍵值對
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>
說明主從是同步正常的.
那么我們?nèi)绾闻袛嗄膫€是主哪個是從呢?我們只需調(diào)用info 這個命令就可以得到主從的信息
了,我們在從庫上執(zhí)行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>
里面有一個角色標(biāo)識,來判斷是主庫還是從庫,對于本例是一個從庫,同時還有一個master_link_status 用于標(biāo)明主從是否異步,如果此值=up,說明同步正常;如果此值=down,
說明同步異步;
db0:keys=1,expires=0, 用于說明數(shù)據(jù)庫有幾個key,以及過期key 的數(shù)量。
事務(wù)控制
redis 對事務(wù)的支持目前還比較簡單。redis 只能保證一個client 發(fā)起的事務(wù)中的命令可以連續(xù)的執(zhí)行,而中間不會插入其他client 的命令。由于redis 是單線程來處理所有client 的請求的所以做到這點是很容易的。一般情況下redis 在接受到一個client 發(fā)來的命令后會立即處理并返回處理結(jié)果,但是當(dāng)一個client 在一個連接中發(fā)出multi 命令,這個連接會進(jìn)入一個事務(wù)上下文,該連接后續(xù)的命令并不是立即執(zhí)行,而是先放到一個隊列中。當(dāng)從此連接受到exec 命令后,redis 會順序的執(zhí)行隊列中的所有命令。并將所有命令的運行結(jié)果打包到一起返回給client.然后此連接就結(jié)束事務(wù)上下文。
簡單事務(wù)控制
下面可以看一個例子
`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 命令發(fā)出后并沒執(zhí)行而是被放到了隊列中。調(diào)用exec后2 個命令才被連續(xù)的執(zhí)行,最后返回的是兩條命令執(zhí)行后的結(jié)果。
如何取消一個事務(wù)
我們可以調(diào)用discard 命令來取消一個事務(wù),讓事務(wù)回滾。接著上面例子
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>
可以發(fā)現(xiàn)這次2 個set age 命令都沒被執(zhí)行。discard 命令其實就是清空事務(wù)的命令隊列并退出事務(wù)上下文,也就是我們常說的事務(wù)回滾。
樂觀鎖復(fù)雜事務(wù)控制
在本小節(jié)開始前,我們有必要向讀者朋友簡單介紹一下樂觀鎖的概念,并舉例說明樂觀鎖是怎么工作的。
樂觀鎖:大多數(shù)是基于數(shù)據(jù)版本(version)的記錄機制實現(xiàn)的。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個版本標(biāo)識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表添加一個“version”字段來實現(xiàn)讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加1。此時,將提交數(shù)據(jù)的版本號與數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本號進(jìn)行比對,如果提交的數(shù)據(jù)版本號大于數(shù)據(jù)庫表當(dāng)前版本號,則予以更新,否則認(rèn)為是過期數(shù)據(jù)。
樂觀鎖實例:假設(shè)數(shù)據(jù)庫中帳戶信息表中有一個version 字段,當(dāng)前值為1;而當(dāng)前帳戶余額字段(balance)為$100。下面我們將用時序表的方式來為大家演示樂觀鎖的實現(xiàn)原理:
操作員A | 操作員B |
---|---|
(1)、操作員A 此時將用戶信息讀出(此時version=1),并準(zhǔn)備從其帳戶余額中扣除$50($100-$50) | (2)、在操作員A 操作的過程中,操作員B 也讀入此用戶信息(此時version=1),并準(zhǔn)備從其帳戶余額中扣除$20($100-$20) |
(3)、操作員A 完成了修改工作,將數(shù)據(jù)版本號加1(此時version=2),連同帳戶扣除后余額(balance=$50),提交至數(shù)據(jù)庫更新,此時由于提交數(shù)據(jù)版本大于數(shù)據(jù)庫記錄當(dāng)前版本,數(shù)據(jù)被更新,數(shù)據(jù)庫記錄version更新為2 | |
(4)、操作員B 完成了操作,也將版本號加1( version=2 ) 并試圖向數(shù)據(jù)庫提交數(shù)據(jù)(balance=$80),但此時比對數(shù)據(jù)庫記錄版本時發(fā)現(xiàn),操作員B 提交的數(shù)據(jù)版本號為2,數(shù)據(jù)庫記錄當(dāng)前版本也為2,不滿足“提交版本必須大于記錄當(dāng)前版本才能執(zhí)行更新”的樂觀鎖策略,因此,操作員B 的提交被駁回 |
這樣,就避免了操作員B 用基于version=1 的舊數(shù)據(jù)修改的結(jié)果來覆蓋操作員A 的操作結(jié)果
的可能。
即然樂觀鎖比悲觀鎖要好很多,redis 是否也支持呢?答案是支持, redis 從2.1.0 開始就支持樂觀鎖了,可以顯式的使用watch 對某個key 進(jìn)行加鎖,避免悲觀鎖帶來的一系列問題。Redis 樂觀鎖實例:假設(shè)有一個age 的key,我們開2 個session 來對age 進(jìn)行賦值操作,我們來看一下結(jié)果如何。
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 的值進(jìn)行修改
第二步,Session 2 已經(jīng)將age 的值設(shè)為30
第三步,Session 1 希望將age 的值設(shè)為20,但結(jié)果一執(zhí)行返回是nil,說明執(zhí)行失敗,之后我們再取一下age 的值是30,這是由于Session 1 中對age 加了樂觀鎖導(dǎo)致的。
watch 命令會監(jiān)視給定的key,當(dāng)exec 時候如果監(jiān)視的key 從調(diào)用watch 后發(fā)生過變化,則整個事務(wù)會失敗。也可以調(diào)用watch 多次監(jiān)視多個key.這樣就可以對指定的key 加樂觀鎖了。注意watch 的key 是對整個連接有效的,事務(wù)也一樣。如果連接斷開,監(jiān)視和事務(wù)都會被自動清除。當(dāng)然了exec,discard,unwatch 命令都會清除連接中的所有監(jiān)視。
redis 的事務(wù)實現(xiàn)是如此簡單,當(dāng)然會存在一些問題。第一個問題是redis 只能保證事務(wù)的每個命令連續(xù)執(zhí)行,但是如果事務(wù)中的一個命令失敗了,并不回滾其他命令,比如使用的命令類型不匹配。下面將以一個實例的例子來說明這個問題:
`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 由于是個數(shù)字,那么它可以有自增運算,但是name 是個字符串,無法對其進(jìn)行自增運算,所以會報錯,如果按傳統(tǒng)關(guān)系型數(shù)據(jù)庫的思路來講,整個事務(wù)都會回滾,但是我們看到redis 卻是將可以執(zhí)行的命令提交了,所以這個現(xiàn)象對于習(xí)慣于關(guān)系型數(shù)據(jù)庫操作的朋友來說是很別扭的,這一點也是redis 今天需要改進(jìn)的地方。
持久化機制
redis 是一個支持持久化的內(nèi)存數(shù)據(jù)庫,也就是說redis 需要經(jīng)常將內(nèi)存中的數(shù)據(jù)同步到磁盤來保證持久化。redis 支持兩種持久化方式,一種是Snapshotting(快照)也是默認(rèn)方式,另一種是Append-only file(縮寫aof)的方式。下面分別介紹:
snapshotting 方式
快照是默認(rèn)的持久化方式。這種方式是就是將內(nèi)存中數(shù)據(jù)以快照的方式寫入到二進(jìn)制文件中,默認(rèn)的文件名為dump.rdb。可以通過配置設(shè)置自動做快照持久化的方式。我們可以配置redis在n 秒內(nèi)如果超過m 個key 被修改就自動做快照,下面是默認(rèn)的快照保存配置
save 900 1 #900 秒內(nèi)如果超過1 個key 被修改,則發(fā)起快照保存
save 300 10 #300 秒內(nèi)容如超過10 個key 被修改,則發(fā)起快照保存
save 60 10000
下面介紹詳細(xì)的快照保存過程:
- redis 調(diào)用fork,現(xiàn)在有了子進(jìn)程和父進(jìn)程。
- 父進(jìn)程繼續(xù)處理client 請求,子進(jìn)程負(fù)責(zé)將內(nèi)存內(nèi)容寫入到臨時文件。由于os 的實時復(fù)制機制(copy on write)父子進(jìn)程會共享相同的物理頁面,當(dāng)父進(jìn)程處理寫請求時os 會為父進(jìn)程要修改的頁面創(chuàng)建副本,而不是寫共享的頁面。所以子進(jìn)程地址空間內(nèi)的數(shù)據(jù)是fork時刻整個數(shù)據(jù)庫的一個快照。
- 當(dāng)子進(jìn)程將快照寫入臨時文件完畢后,用臨時文件替換原來的快照文件,然后子進(jìn)程退出。
client 也可以使用save 或者bgsave 命令通知redis 做一次快照持久化。save 操作是在主線程中保存快照的,由于redis 是用一個主線程來處理所有client 的請求,這種方式會阻塞所有client 請求。所以不推薦使用。另一點需要注意的是,每次快照持久化都是將內(nèi)存數(shù)據(jù)完整寫入到磁盤一次,并不是增量的只同步變更數(shù)據(jù)。如果數(shù)據(jù)量大的話,而且寫操作比較多,必然會引起大量的磁盤io操作,可能會嚴(yán)重影響性能。
下面將演示各種場景的數(shù)據(jù)庫持久化情況
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
我們先設(shè)置了一個name 的鍵值對,然后正常關(guān)閉了數(shù)據(jù)庫實例,數(shù)據(jù)是否被保存到磁盤了
呢?我們來看一下服務(wù)器端是否有消息被記錄下來了:
[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]#
從日志可以看出,數(shù)據(jù)庫做了一個存盤的操作,將內(nèi)存的數(shù)據(jù)寫入磁盤了。正常的話,磁盤
上會產(chǎn)生一個dump 文件,用于保存數(shù)據(jù)庫快照,我們來驗證一下:
[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]#
硬盤上已經(jīng)產(chǎn)生了一個數(shù)據(jù)庫快照了。這時侯我們再將redis 啟動,看鍵值還是否真的持久
化到硬盤了。
`redis 127.0.0.1:6379> keys *
- "name"
redis 127.0.0.1:6379> get name
"HongWan"
redis 127.0.0.1:6379>`
數(shù)據(jù)被完全持久化到硬盤了。
aof 方式
另外由于快照方式是在一定間隔時間做一次的,所以如果redis 意外down 掉的話,就會丟失最后一次快照后的所有修改。如果應(yīng)用要求不能丟失任何修改的話,可以采用aof 持久化方式。
下面介紹Append-only file:
aof 比快照方式有更好的持久化性,是由于在使用aof 持久化方式時,redis 會將每一個收到
的寫命令都通過write 函數(shù)追加到文件中(默認(rèn)是appendonly.aof)。當(dāng)redis 重啟時會通過重
新執(zhí)行文件中保存的寫命令來在內(nèi)存中重建整個數(shù)據(jù)庫的內(nèi)容。當(dāng)然由于os 會在內(nèi)核中緩
存 write 做的修改,所以可能不是立即寫到磁盤上。這樣aof 方式的持久化也還是有可能會
丟失部分修改。不過我們可以通過配置文件告訴redis 我們想要通過fsync 函數(shù)強制os 寫入
到磁盤的時機。
有三種方式如下(默認(rèn)是:每秒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>`
我們先設(shè)置2 個鍵值對,然后我們看一下系統(tǒng)中有沒有產(chǎn)生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]#
結(jié)果證明產(chǎn)生了,接著我們將redis 再次啟動后來看一下數(shù)據(jù)是否還在
`[root@localhost redis-2.2.12]# src/redis-cli
redis 127.0.0.1:6379> keys *
- "age"
- "name"
redis 127.0.0.1:6379>`
數(shù)據(jù)還存在系統(tǒng)中,說明系統(tǒng)是在啟動時執(zhí)行了一下從磁盤到內(nèi)存的load 數(shù)據(jù)的過程。
aof 的方式也同時帶來了另一個問題。持久化文件會變的越來越大。例如我們調(diào)用incr test命令100 次,文件中必須保存全部的100 條命令,其實有99 條都是多余的。因為要恢復(fù)數(shù)據(jù)庫的狀態(tài)其實文件中保存一條set test 100 就夠了。為了壓縮aof 的持久化文件。redis 提供了bgrewriteaof 命令。收到此命令redis 將使用與快照類似的方式將內(nèi)存中的數(shù)據(jù)以命令的方式保存到臨時文件中,最后替換原來的文件。
具體過程如下
1、redis 調(diào)用fork ,現(xiàn)在有父子兩個進(jìn)程
2、子進(jìn)程根據(jù)內(nèi)存中的數(shù)據(jù)庫快照,往臨時文件中寫入重建數(shù)據(jù)庫狀態(tài)的命令
3、父進(jìn)程繼續(xù)處理client 請求,除了把寫命令寫入到原來的aof 文件中。同時把收到的寫命令緩存起來。這樣就能保證如果子進(jìn)程重寫失敗的話并不會出問題。
4、當(dāng)子進(jìn)程把快照內(nèi)容寫入以命令方式寫到臨時文件中后,子進(jìn)程發(fā)信號通知父進(jìn)程。然后父進(jìn)程把緩存的寫命令也寫入到臨時文件。
5、現(xiàn)在父進(jìn)程可以使用臨時文件替換老的aof 文件,并重命名,后面收到的寫命令也開始往新的aof 文件中追加。
需要注意到是重寫aof 文件的操作,并沒有讀取舊的aof 文件,而是將整個內(nèi)存中的數(shù)據(jù)庫內(nèi)容用命令的方式重寫了一個新的aof 文件,這點和快照有點類似。接來我們看一下實際的例子:
我們先調(diào)用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 個字節(jié),接下來我們調(diào)用一下bgrewriteaof 命令將內(nèi)存中的數(shù)據(jù)重新刷到磁盤的
日志文件中
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
日志文件大小變?yōu)?27 個字節(jié)了,說明原來日志中的重復(fù)記錄已被刷新掉了。
發(fā)布及訂閱消息
發(fā)布訂閱(pub/sub)是一種消息通信模式,主要的目的是解耦消息發(fā)布者和消息訂閱者之間的耦合,這點和設(shè)計模式中的觀察者模式比較相似。pub/sub 不僅僅解決發(fā)布者和訂閱者直接代碼級別耦合也解決兩者在物理部署上的耦合。redis 作為一個pub/sub 的server,在訂閱者和發(fā)布者之間起到了消息路由的功能。訂閱者可以通過subscribe 和psubscribe 命令向redis server 訂閱自己感興趣的消息類型,redis 將消息類型稱為通道(channel)。當(dāng)發(fā)布者通過publish 命令向redis server 發(fā)送特定類型的消息時。訂閱該消息類型的全部client 都會收到此消息。這里消息的傳遞是多對多的。一個client 可以訂閱多個channel,也可以向多個channel發(fā)送消息。
下面做個實驗。這里使用3 不同的client, client1 用于訂閱tv1 這個channel 的消息,client2用于訂閱tv1 和tv2 這2 個chanel 的消息,client3 用于發(fā)布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" |
下面將詳細(xì)的解釋一下上面的例子
1、client1 訂閱了tv1 這個channel 這個頻道的消息,client2 訂閱了tv1 和tv2 這2 個頻道的消息
2、client3 是用于發(fā)布tv1 和tv2 這2 個頻道的消息發(fā)布者
3、接下來我們在client3 發(fā)布了一條消息”publish tv1 program1”,大家可以看到這條消息是發(fā)往tv1 這個頻道的
4、理所當(dāng)然的client1 和client2 都接收到了這個頻道的消息
5、 然后client3 又發(fā)布了一條消息”publish tv2 program2”,這條消息是發(fā)往tv2 的,由于client1 并沒有訂閱tv1,所以client1 的結(jié)果中并沒有顯示出任何結(jié)果,但client2 訂閱了這個頻道,所以client2 是會有返回結(jié)果的。
我們也可以用psubscribe tv*的方式批量訂閱以tv 開頭的頻道的內(nèi)容。
看完這個小例子后應(yīng)該對pub/sub 功能有了一個感性的認(rèn)識。需要注意的是當(dāng)一個連接通過subscribe 或者psubscribe 訂閱通道后就進(jìn)入訂閱模式。在這種模式除了再訂閱額外的通道或者用unsubscribe 或者punsubscribe 命令退出訂閱模式,就不能再發(fā)送其他命令。另外使用psubscribe 命令訂閱多個通配符通道,如果一個消息匹配上了多個通道模式的話,會多次收到同一個消息。
Pipeline 批量發(fā)送請求
redis 是一個cs 模式的tcp server,使用和http 類似的請求響應(yīng)協(xié)議。一個client 可以通過一個socket 連接發(fā)起多個請求命令。每個請求命令發(fā)出后client 通常會阻塞并等待redis 服務(wù)處理,redis 處理完后請求命令后會將結(jié)果通過響應(yīng)報文返回給client。基本的通信過程如下:
Client: INCR X Server: 1 Client: INCR X Server: 2 Client: INCR X Server: 3 Client: INCR X Server: 4
基本上四個命令需要8 個tcp 報文才能完成。由于通信會有網(wǎng)絡(luò)延遲,假如從client 和server之間的包傳輸時間需要0.125 秒。那么上面的四個命令8 個報文至少會需要1 秒才能完成。這樣即使redis 每秒能處理100 個命令,而我們的client 也只能一秒鐘發(fā)出四個命令。這顯示沒有充分利用redis 的處理能力,怎么樣解決這個問題呢? 我們可以利用pipeline 的方式從client 打包多條命令一起發(fā)出,不需要等待單條命令的響應(yīng)返回,而redis 服務(wù)端會處理完多條命令后會將多條命令的處理結(jié)果打包到一起返回給客戶端。通信過程如下
Client: INCR X Client: INCR X Client: INCR X Client: INCR X Server: 1 Server: 2 Server: 3 Server: 4
假設(shè)不會因為tcp 報文過長而被拆分。可能兩個tcp 報文就能完成四條命令,client 可以將四個incr 命令放到一個tcp 報文一起發(fā)送,server 則可以將四條命令的處理結(jié)果放到一個tcp報文返回。通過pipeline 方式當(dāng)有大批量的操作時候,我們可以節(jié)省很多原來浪費在網(wǎng)絡(luò)延遲的時間,需要注意到是用pipeline 方式打包命令發(fā)送,redis 必須在處理完所有命令前先緩存起所有命令的處理結(jié)果。打包的命令越多,緩存消耗內(nèi)存也越多。所以并不是打包的命令越多越好。具體多少合適需要根據(jù)具體情況測試。下面是個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 方式發(fā)送指令 usePipeline(); long end = System.currentTimeMillis(); System.out.println("用pipeline 方式耗時:" + (end - start) + "毫秒"); start = System.currentTimeMillis(); //普通方式發(fā)送指令 withoutPipeline(); end = System.currentTimeMillis(); System.out.println("普通方式耗時:" + (end - start) + "毫秒"); } //采用pipeline 方式發(fā)送指令 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) { } } //普通方式發(fā)送指令 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) { } } }
執(zhí)行結(jié)果如下:
-- 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 毫秒
所以用兩種方式發(fā)送指令,耗時是不一樣的,具體是否使用pipeline 必須要基于大家手中的網(wǎng)絡(luò)情況來決定,不能一切都按最新最好的技術(shù)來實施,因為它有可能不是最適合你的。
虛擬內(nèi)存的使用
首先說明下redis 的虛擬內(nèi)存與操作系統(tǒng)的虛擬內(nèi)存不是一碼事,但是思路和目的都是相同的。就是暫時把不經(jīng)常訪問的數(shù)據(jù)從內(nèi)存交換到磁盤中,從而騰出寶貴的內(nèi)存空間用于其他需要訪問的數(shù)據(jù)。尤其是對于redis 這樣的內(nèi)存數(shù)據(jù)庫,內(nèi)存總是不夠用的。除了可以將數(shù)據(jù)分割到多個redis server 外。另外的能夠提高數(shù)據(jù)庫容量的辦法就是使用虛擬內(nèi)存把那些不經(jīng)常訪問的數(shù)據(jù)交換到磁盤上。如果我們的存儲的數(shù)據(jù)總是有少部分?jǐn)?shù)據(jù)被經(jīng)常訪問,大部分?jǐn)?shù)據(jù)很少被訪問,對于網(wǎng)站來說確實總是只有少量用戶經(jīng)常活躍。當(dāng)少量數(shù)據(jù)被經(jīng)常訪問時,使用虛擬內(nèi)存不但能提高單臺redis server 數(shù)據(jù)庫的容量,而且也不會對性能造成太多影響。
redis 沒有使用操作系統(tǒng)提供的虛擬內(nèi)存機制而是自己在實現(xiàn)了自己的虛擬內(nèi)存機制,主要的理由有兩點:
- 操作系統(tǒng)的虛擬內(nèi)存是以4k 頁面為最小單位進(jìn)行交換的。而redis 的大多數(shù)對象都遠(yuǎn)小于4k,所以一個操作系統(tǒng)頁面上可能有多個redis 對象。另外redis 的集合對象類型如list,set可能存在于多個操作系統(tǒng)頁面上。最終可能造成只有10%key 被經(jīng)常訪問,但是所有操作系統(tǒng)頁面都會被操作系統(tǒng)認(rèn)為是活躍的,這樣只有內(nèi)存真正耗盡時操作系統(tǒng)才會交換頁面。
2、相比于操作系統(tǒng)的交換方式,redis 可以將被交換到磁盤的對象進(jìn)行壓縮,保存到磁盤的對象可以去除指針和對象元數(shù)據(jù)信息,一般壓縮后的對象會比內(nèi)存中的對象小10 倍,這樣redis的虛擬內(nèi)存會比操作系統(tǒng)虛擬內(nèi)存能少做很多io 操作。
下面是vm 相關(guān)配置
vm-enabled yes #開啟vm 功能
vm-swap-file /tmp/redis.swap #交換出來的value 保存的文件路徑
vm-max-memory 1000000 #redis 使用的最大內(nèi)存上限
vm-page-size 32 #每個頁面的大小32 個字節(jié)
vm-pages 134217728 #最多使用多少頁面
vm-max-threads 4 #用于執(zhí)行value 對象換入換出的工作線程數(shù)量
redis 的虛擬內(nèi)存在設(shè)計上為了保證key 的查找速度,只會將value 交換到swap 文件中。所以如果是內(nèi)存問題是由于太多value 很小的key 造成的,那么虛擬內(nèi)存并不能解決,和操作系統(tǒng)一樣redis 也是按頁面來交換對象的。redis 規(guī)定同一個頁面只能保存一個對象。但是一個對象可以保存在多個頁面中。在redis 使用的內(nèi)存沒超過vm-max-memory 之前是不會交換任何value 的。當(dāng)超過最大內(nèi)存限制后,redis 會選擇較過期的對象。如果兩個對象一樣過期會優(yōu)先交換比較大的對象,精確的公式swappability = age*log(size_in_memory)。對于vm-page-size 的設(shè)置應(yīng)該根據(jù)自己的應(yīng)用將頁面的大小設(shè)置為可以容納大多數(shù)對象的大小,太大了會浪費磁盤空間,太小了會造成交換文件出現(xiàn)碎片。對于交換文件中的每個頁面,redis
會在內(nèi)存中對應(yīng)一個1bit 值來記錄頁面的空閑狀態(tài)。所以像上面配置中頁面數(shù)量(vm-pages 134217728 )會占用16M 內(nèi)存用來記錄頁面空閑狀態(tài)。vm-max-threads 表示用做交換任務(wù)的線程數(shù)量。如果大于0 推薦設(shè)為服務(wù)器的cpu 內(nèi)核的數(shù)量,如果是0 則交換過程在主線程進(jìn)行。