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