redis是一個key-value存儲系統。和Memcached相似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)和zset(有序集合)。這些數據類型都支持push/pop、add/remove及取交集並集和差集及更豐富的操做,並且這些操做都是原子性的。在此基礎上,redis支持各類不一樣方式的排序。與memcached同樣,爲了保證效率,數據都是緩存在內存中。區別的是redis會週期性的把更新的數據寫入磁盤或者把修改操做寫入追加的記錄文件,而且在此基礎上實現了master-slave(主從)同步。html
redis是當前比較熱門的NOSQL系統之一,它是一個key-value存儲系統。和Memcached相似,但很大程度補償了memcached的不足,它支持存儲的value類型相對更多,包括string、list、set、zset和hash。這些數據類型都支持push/pop、add/remove及取交集並集和差集及更豐富的操做。在此基礎上,redis支持各類不一樣方式的排序。Redis數據都是緩存在計算機內存中,而且會週期性的把更新的數據寫入磁盤或者把修改操做寫入追加的記錄文件。mysql
Redis是一個高性能的key-value內存數據庫。官方性能測試結果:
set操做每秒110000次,get操做每秒81000次。 linux
和Memcached相似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)和zset(有序集合)。這些數據類型都支持push/pop、add/remove及取交集並集和差集及更豐富的操做。 redis
Replication(樹形) 算法
data types(String、Lists、Sorted Sets、Hashes) sql
persistence (snapshot、aof) 數據庫
不少開發者都認爲Redis不可能比Memcached快,Memcached徹底基於內存,而Redis具備持久化保存特性,即便是異步的,Redis也不可能比Memcached快。可是測試結果基本是Redis佔絕對優點。一直在思考這個緣由,目前想到的緣由有這幾方面。編程
Libevent。和Memcached不一樣,Redis並無選擇libevent。Libevent爲了迎合通用性形成代碼龐大(目前Redis代碼還不到libevent的1/3)及犧牲了在特定平臺的很多性能。Redis用libevent中兩個文件修改實現了本身的epoll event loop(4)。業界很多開發者也建議Redis使用另一個libevent高性能替代libev,可是做者仍是堅持Redis應該小巧並去依賴的思路。一個印象深入的細節是編譯Redis以前並不須要執行./configure。vim
CAS問題。CAS是Memcached中比較方便的一種防止競爭修改資源的方法。CAS實現須要爲每一個cache key設置一個隱藏的cas token,cas至關value版本號,每次set會token須要遞增,所以帶來CPU和內存的雙重開銷,雖然這些開銷很小,可是到單機10G+ cache以及QPS上萬以後這些開銷就會給雙方相對帶來一些細微性能差異(5)。設計模式
Redis的數據所有放在內存帶來了高速的性能,可是也帶來一些不合理之處。好比一箇中型網站有100萬註冊用戶,若是這些資料要用Redis來存儲,內存的容量必須可以容納這100萬用戶。可是業務實際狀況是100萬用戶只有5萬活躍用戶,1周來訪問過1次的也只有15萬用戶,所以所有100萬用戶的數據都放在內存有不合理之處,RAM須要爲冷數據買單。
這跟操做系統很是類似,操做系統全部應用訪問的數據都在內存,可是若是物理內存容納不下新的數據,操做系統會智能將部分長期沒有訪問的數據交換到磁盤,爲新的應用留出空間。現代操做系統給應用提供的並非物理內存,而是虛擬內存(Virtual Memory)的概念。
基於相同的考慮,Redis 2.0也增長了VM特性。讓Redis數據容量突破了物理內存的限制。並實現了數據冷熱分離。
Redis的VM依照以前的epoll實現思路依舊是本身實現。可是在前面操做系統的介紹提到OS也能夠自動幫程序實現冷熱數據分離,Redis只須要OS申請一塊大內存,OS會自動將熱數據放入物理內存,冷數據交換到硬盤,另一個知名的"理解了現代操做系統(3)"的Varnish就是這樣實現,也取得了很是成功的效果。
做者antirez在解釋爲何要本身實現VM中提到幾個緣由(6)。主要OS的VM換入換出是基於Page概念,好比OS VM1個Page是4K, 4K中只要還有一個元素即便只有1個字節被訪問,這個頁也不會被SWAP, 換入也一樣道理,讀到一個字節可能會換入4K無用的內存。而Redis本身實現則能夠達到控制換入的粒度。另外訪問操做系統SWAP內存區域時block進程,也是致使Redis要本身實現VM緣由之一。
做爲一個key value存在,不少開發者天然的使用set/get方式來使用Redis,實際上這並非最優化的使用方法。尤爲在未啓用VM狀況下,Redis所有數據須要放入內存,節約內存尤爲重要。
假如一個key-value單元須要最小佔用512字節,即便只存一個字節也佔了512字節。這時候就有一個設計模式,能夠把key複用,幾個key-value放入一個key中,value再做爲一個set存入,這樣一樣512字節就會存放10-100倍的容量。
這就是爲了節約內存,建議使用hashset而不是set/get的方式來使用Redis
Redis有兩種存儲方式,默認是snapshot方式,實現方法是定時將內存的快照(snapshot)持久化到硬盤,這種方法缺點是持久化以後若是出現crash則會丟失一段數據。所以在完美主義者的推進下做者增長了aof方式。aof即append only mode,在寫入內存數據的同時將操做命令保存到日誌文件,在一個併發更改上萬的系統中,命令日誌是一個很是龐大的數據,管理維護成本很是高,恢復重建時間會很是長,這樣致使失去aof高可用性本意。另外更重要的是Redis是一個內存數據結構模型,全部的優點都是創建在對內存複雜數據結構高效的原子操做上,這樣就看出aof是一個很是不協調的部分。
其實aof目的主要是數據可靠性及高可用性,在Redis中有另一種方法來達到目的:Replication。因爲Redis的高性能,複製基本沒有延遲。這樣達到了防止單點故障及實現了高可用。
支持
redis主從複製配置和使用都很是簡單。經過主從複製能夠容許多個slave server擁有和master server相同的數據庫副本。下面是關於redis主從複製的一些特色
1.master能夠有多個slave
2.除了多個slave連到相同的master外,slave也能夠鏈接其餘slave造成圖狀結構
3.主從複製不會阻塞master。也就是說當一個或多個slave與master進行初次同步數據時,master能夠繼續處理client發來的請求。相反slave在初次同步數據時則會阻塞不能處理client的請求。
4.主從複製能夠用來提升系統的可伸縮性,咱們能夠用多個slave 專門用於client的讀請求,好比sort操做可使用slave來處理。也能夠用來作簡單的數據冗餘
5.能夠在master禁用數據持久化,只須要註釋掉master 配置文件中的全部save配置,而後只在slave上配置數據持久化。
下面介紹下主從複製的過程
當設置好slave服務器後,slave會創建和master的鏈接,而後發送sync命令。不管是第一次同步創建的鏈接仍是鏈接斷開後的從新連接,master都會啓動一個後臺進程,將數據庫快照保存到文件中,同時master主進程會開始收集新的寫命令並緩存起來。後臺進程完成寫文件後,master就發送文件給slave,slave將文件保存到磁盤上,而後加載到內存恢復數據庫快照到slave上。接着master就會把緩存的命令轉發給slave。並且後續master收到的寫命令都會經過開始創建的鏈接發送給slave。從master到slave的同步數據的命令和從 client發送的命令使用相同的協議格式。當master和slave的鏈接斷開時slave能夠自動從新創建鏈接。若是master同時收到多個 slave發來的同步鏈接命令,只會使用啓動一個進程來寫數據庫鏡像,而後發送給全部slave。
Redis的主從複製功能很是強大,一個master能夠擁有多個slave,而一個slave又能夠擁有多個slave,如此下去,造成了強大的多級服務器集羣架構。下面我演示下怎樣在多臺服務器上進行Redis數據主從複製。這裏我假設有兩臺服務器,一臺是Windows操做系統(局域網IP:192.168.3.82),一臺是Linux操做系統(局域網IP:192.168.3.90),在兩個操做系統都安裝redis,Windows操做系統使用cygwin工具進行安裝,命令爲:
1 |
$ tarxzf redis-2.2.2.tar.gz |
2 |
$ cdredis-2.2.2 |
3 |
$ make |
能夠經過"make test"命令判斷是否安裝成功。
這裏我使用1個master以及2個slave(master在Windows下,一個slave在Windows下,一個slave在Linux下),基本流程是:
1. 在Windows服務器上建立兩個目錄,Demo1,Demo2,其中Demo1用來存放Master服務,Demo2用來存放Slave服務,
在Master服務中的配置文件修改:
1 |
bind 192.168.3.82 |
在Slave服務中的配置文件修改:
1 |
port 6381(服務端口號要分開) |
2 |
bind 192.168.3.82 |
3 |
slaveof 192.168.3.82 6379 (設置master的Host以及Port) |
2. 在Linux服務器上建立一個目錄,Demo,Demo存放Slave服務,在服務中的配置文件修改:
1 |
bind 192.168.3.90 |
2 |
slaveof 192.168.3.82 6379(設置master的Host以及Port) |
這樣就完成了全部的配置。
3. 如今運行這3個服務,經過命令:
1 |
./redis-server redis.conf |
來啓動redis服務。
注意到,當我啓動master,而後啓動一個slave的時候,能夠發現slave上:
會發送一個SYNC請求,從Master上面進行相應,並且它支持自動重連,即當master掉線的狀況下,它會處於等待請求的狀態。
而Master上:
可以接受Slave的應答,而且開始持久化操做,說明在Slave每次去鏈接Master的時候,都會去持久化磁盤。
4. 如今開始寫一個客戶端程序,使用到ServiceStack.Redis.dll的.NET組件:
01 |
usingServiceStack.Redis; |
02 |
|
03 |
staticvoidMain(string[] args) |
04 |
{ |
05 |
IRedisClientFactory factory = newRedisCacheClientFactory(); |
06 |
IRedisClient client = factory.CreateRedisClient("192.168.3.82", 6379); |
07 |
|
08 |
client.Set<STRING>("username", "leepy"); |
09 |
|
10 |
stringusername = client.Get<STRING>("username"); |
11 |
|
12 |
client.Save(); |
13 |
|
14 |
Console.WriteLine("username: {0}", username); |
15 |
|
16 |
Console.ReadLine(); |
17 |
} |
運行結果:
數據Set的時候,數據保存在內存中,當調用Save方法時候,將數據保存在磁盤中。
其中你會發如今3個服務目錄中,都出現了dump.rdb,說明Master的文件都同步到Slave中去了。
用UE編輯器打開文件查看:
從Redis源碼中,能夠發現rdb文件採用的是lzf壓縮算法進行實現,默認lzf壓縮算法是開啓的。
1、 下載安裝
Wget http://redis.googlecode.com/files/redis-2.2.7.tar.gz
2、.安裝部署
tar zxvf
redis-2.2.7.tar.gz
cd redis-2.2.7.tar.gz
make
能夠將redis.conf 複製到 /etc/下
cp redis.conf /etc/
cp src/redis-server src/redis-cli src/redis-benchmark /usr/local/redis
啓動redis
vim /etc/redis.conf
1.redis 的配置文件單位比較有意思,對1k 和1kb 這樣的單位作了明確的區分,不過感受意義不是很大。
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
並且,單位不區分大小寫,1M = 1m
2.daemonize no
默認下,redis啓動不以守護進程的方式啓動,生產環境下建議將配置改成yes
3.pidfile /var/run/redis.pid,pid文件,沒啥好說的
4.port 6379,配置redis端口,默認6379。能夠將port配置爲0,此時不監聽tcp協議,有些怪怪的。
5.bind 127.0.0.1,綁定ip。默認綁定全部本機ip,通常用在服務器多ip下,能夠只監聽內網服務器ip,保證服務安全。
6.unixsocket /tmp/redis.sock。sock文件
7.timeout 300。客戶端超時時間,單位秒
8.loglevel verbose
log級別,支持四個級別,debug,notice,verbose,warning
9.logfile log文件路徑
logfile stdout,log輸出到標準設備,logs不寫文件,輸出到空設備,/deb/null
10.SNAPSHOTTING 快照,持久化相關
save <seconds> <changes>
save 900 1
保存快照的頻率,表示:在多長時間內執行必定數量的寫操做時,保存快照的頻率,能夠設置多個條件。若是都註釋掉,則不作內存數據持久化。本環境就是把redis只用做cache,不開啓持久化功能。
rdbcompression:是否使用壓縮
dbfilename:快照數據庫名稱
dir:#數據庫存放路徑
11. REPLICATION 主從配置(配置從服務器)
slaveof <masterip> <masterport>: 主庫ip 端口
masterauth <master-password>:主庫服務器口令,若是主服務器未打開requirepass,怎不須要此項
slave-serve-stale-data yes:在master服務器掛掉或者同步失敗時,從服務器是否繼續提供服務。
12.########## SECURITY ########
安全相關
requirepass foobared:設置redis服務密碼,若是開啓,則客戶端鏈接時須要 -a 指定密碼,不然操做會提示無權限
rename-command : 命令更名,至關於linux alias
能夠用改功能屏蔽一些危險命令
如rename-command CONFIG ""。別名爲空就可。
13.LIMITS
maxclients 128:最大鏈接數;0 表示不限制
maxmemory <bytes> : 最大使用內存,推薦生產環境下作相應調整,咱們用的是隻用來作高速緩存,限制2G。默認狀況下,redis會佔用可用的全部內存。
maxmemory-samples 3: 不是很明白,說是校驗 lru ttl 算法準確性的。
14.APPEND ONLY MODE
appendonly:#是否開啓appendonlylog,開啓的話每次寫操做會記一條log。至關於mysql的binlog;不一樣的是,每次redis啓動都會讀此文件構建完整數據。即便刪除rdb文件,數據也是安全的。
appendfilename appendonly.aof:日誌文件的名稱,默認appendonly.aof
appendfsync:異步寫append file 的策略。相似mysql事物log寫方式。三種
appendfsync always:同步,每次寫都要flush到磁盤,安全,速度慢。
appendfsync everysec :每秒寫(默認值,推薦值)同mysql
appendfsync no:交給操做系統去作flush的動做
no-appendfsync-on-rewrite no:不是很清楚,防止linux阻止長耗時io問題的?
15 VIRTUAL MEMORY ,虛擬內存設置
vm-enabled no : 虛擬內存開關
vm-swap-file /tmp/redis.swap: swap文件,不一樣redis swap文件不能共享。並且生產環境下,不建議放在tmp目錄。
vm-max-memory 0: vm大小限制。0:不限制,建議60-80% 可用內存大小。
vm-page-size 32 : 根據緩存內容大小調整,默認32字節。
vm-pages 134217728 : page數。每 8 page,會佔用1字節內存。
vm-page-size * vm-pages 等於 swap 文件大小
vm-max-threads 4 : vm 最大io線程數。注意: 0 標誌禁止使用vm(開關真多)
16.ADVANCED CONFIG ,高級配置
hash-max-zipmap-entries 512
hash-max-zipmap-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
activerehashing yes
Port默認端口是 6379
簡單的測試:
存值:
./redis-cli set hx aaa
取值:
./redis-cli get hx
要配置參數的意義:
Redis提供了豐富的命令(command)對數據庫和各類數據類型進行操做,這些command能夠在Linux終端使用。在編程時,好比使用Redis 的Java語言包,這些命令都有對應的方法,好比上面例子中使用的sadd方法,就是對集合操做中的SADD命令。下面將Redis提供的命令作一總結。
quit:關閉鏈接(connection)
auth:簡單密碼認證
exists(key):確認一個key是否存在
del(key):刪除一個key
type(key):返回值的類型
keys(pattern):返回知足給定pattern的全部key
randomkey:隨機返回key空間的一個key
rename(oldname, newname):將key由oldname重命名爲newname,若newname存在則刪除newname表示的key
dbsize:返回當前數據庫中key的數目
expire:設定一個key的活動時間(s)
ttl:得到一個key的活動時間
select(index):按索引查詢
move(key, dbindex):將當前數據庫中的key轉移到有dbindex索引的數據庫
flushdb:刪除當前選擇數據庫中的全部key
flushall:刪除全部數據庫中的全部key
set(key, value):給數據庫中名稱爲key的string賦予值value
get(key):返回數據庫中名稱爲key的string的value
getset(key, value):給名稱爲key的string賦予上一次的value
mget(key1, key2,…, key N):返回庫中多個string(它們的名稱爲key1,key2…)的value
setnx(key, value):若是不存在名稱爲key的string,則向庫中添加string,名稱爲key,值爲value
setex(key, time, value):向庫中添加string(名稱爲key,值爲value)同時,設定過時時間time
mset(key1, value1, key2, value2,…key N, value N):同時給多個string賦值,名稱爲key i的string賦值value i
msetnx(key1, value1, key2, value2,…key N, value N):若是全部名稱爲key i的string都不存在,則向庫中添加string,名稱key i賦值爲value i
incr(key):名稱爲key的string增1操做
incrby(key, integer):名稱爲key的string增長integer
decr(key):名稱爲key的string減1操做
decrby(key, integer):名稱爲key的string減小integer
append(key, value):名稱爲key的string的值附加value
substr(key, start, end):返回名稱爲key的string的value的子串
rpush(key, value):在名稱爲key的list尾添加一個值爲value的元素
lpush(key, value):在名稱爲key的list頭添加一個值爲value的 元素
llen(key):返回名稱爲key的list的長度
lrange(key, start, end):返回名稱爲key的list中start至end之間的元素(下標從0開始,下同)
ltrim(key, start, end):截取名稱爲key的list,保留start至end之間的元素
lindex(key, index):返回名稱爲key的list中index位置的元素
lset(key, index, value):給名稱爲key的list中index位置的元素賦值爲value
lrem(key, count, value):刪除count個名稱爲key的list中值爲value的元素。count爲0,刪除全部值爲value的元素,count>0從頭到尾刪除count個值爲value的元素,count<0從尾到頭刪除|count|個值爲value的元素。
lpop(key):返回並刪除名稱爲key的list中的首元素
rpop(key):返回並刪除名稱爲key的list中的尾元素
blpop(key1, key2,… key N, timeout):lpop命令的block版本。即當timeout爲0時,若遇到名稱爲key i的list不存在或該list爲空,則命令結束。若是timeout>0,則遇到上述狀況時,等待timeout秒,若是問題沒有解決,則對key i+1開始的list執行pop操做。
brpop(key1, key2,… key N, timeout):rpop的block版本。參考上一命令。
rpoplpush(srckey, dstkey):返回並刪除名稱爲srckey的list的尾元素,並將該元素添加到名稱爲dstkey的list的頭部
sadd(key, member):向名稱爲key的set中添加元素member
srem(key, member) :刪除名稱爲key的set中的元素member
spop(key) :隨機返回並刪除名稱爲key的set中一個元素
smove(srckey, dstkey, member) :將member元素從名稱爲srckey的集合移到名稱爲dstkey的集合
scard(key) :返回名稱爲key的set的基數
sismember(key, member) :測試member是不是名稱爲key的set的元素
sinter(key1, key2,…key N) :求交集
sinterstore(dstkey, key1, key2,…key N) :求交集並將交集保存到dstkey的集合
sunion(key1, key2,…key N) :求並集
sunionstore(dstkey, key1, key2,…key N) :求並集並將並集保存到dstkey的集合
sdiff(key1, key2,…key N) :求差集
sdiffstore(dstkey, key1, key2,…key N) :求差集並將差集保存到dstkey的集合
smembers(key) :返回名稱爲key的set的全部元素
srandmember(key) :隨機返回名稱爲key的set的一個元素
zadd(key, score, member):向名稱爲key的zset中添加元素member,score用於排序。若是該元素已經存在,則根據score更新該元素的順序。
zrem(key, member) :刪除名稱爲key的zset中的元素member
zincrby(key, increment, member) :若是在名稱爲key的zset中已經存在元素member,則該元素的score增長increment;不然向集合中添加該元素,其score的值爲increment
zrank(key, member) :返回名稱爲key的zset(元素已按score從小到大排序)中member元素的rank(即index,從0開始),若沒有member元素,返回"nil"
zrevrank(key, member) :返回名稱爲key的zset(元素已按score從大到小排序)中member元素的rank(即index,從0開始),若沒有member元素,返回"nil"
zrange(key, start, end):返回名稱爲key的zset(元素已按score從小到大排序)中的index從start到end的全部元素
zrevrange(key, start, end):返回名稱爲key的zset(元素已按score從大到小排序)中的index從start到end的全部元素
zrangebyscore(key, min, max):返回名稱爲key的zset中score >= min且score <= max的全部元素
zcard(key):返回名稱爲key的zset的基數
zscore(key, element):返回名稱爲key的zset中元素element的score
zremrangebyrank(key, min, max):刪除名稱爲key的zset中rank >= min且rank <= max的全部元素
zremrangebyscore(key, min, max) :刪除名稱爲key的zset中score >= min且score <= max的全部元素
zunionstore / zinterstore(dstkeyN, key1,…,keyN, WEIGHTS w1,…wN, AGGREGATE SUM|MIN|MAX):對N個zset求並集和交集,並將最後的集合保存在dstkeyN中。對於集合中每個元素的score,在進行AGGREGATE運算前,都要乘以對於的WEIGHT參數。若是沒有提供WEIGHT,默認爲1。默認的AGGREGATE是SUM,即結果集合中元素的score是全部集合對應元素進行SUM運算的值,而MIN和MAX是指,結果集合中元素的score是全部集合對應元素中最小值和最大值。
hset(key, field, value):向名稱爲key的hash中添加元素field<—>value
hget(key, field):返回名稱爲key的hash中field對應的value
hmget(key, field1, …,field N):返回名稱爲key的hash中field i對應的value
hmset(key, field1, value1,…,field N, value N):向名稱爲key的hash中添加元素field i<—>value i
hincrby(key, field, integer):將名稱爲key的hash中field的value增長integer
hexists(key, field):名稱爲key的hash中是否存在鍵爲field的域
hdel(key, field):刪除名稱爲key的hash中鍵爲field的域
hlen(key):返回名稱爲key的hash中元素個數
hkeys(key):返回名稱爲key的hash中全部鍵
hvals(key):返回名稱爲key的hash中全部鍵對應的value
hgetall(key):返回名稱爲key的hash中全部的鍵(field)及其對應的value
save:將數據同步保存到磁盤
bgsave:將數據異步保存到磁盤
lastsave:返回上次成功將數據保存到磁盤的Unix時戳
shundown:將數據同步保存到磁盤,而後關閉服務
info:提供服務器的信息和統計
monitor:實時轉儲收到的請求
slaveof:改變複製策略設置
config:在運行時配置Redis服務器
在192.168.134.96 上安裝了redis的master
在192.168.134.97 上安裝了slave 綁定了192.168.134.96
redis對事務的支持目前還比較簡單。redis只能保證一個client發起的事務中的命令能夠連續的執行,而中間不會插入其餘client的命令。因爲redis是單線程來處理全部client的請求的因此作到這點是很容易的。通常狀況下redis在接受到一個client發來的命令後會當即處理並返回處理結果,可是當一個client在一個鏈接中發出multi命令有,這個鏈接會進入一個事務上下文,該鏈接後續的命令並非當即執行,而是先放到一個隊列中。當今後鏈接受到exec命令後,redis會順序的執行隊列中的全部命令。並將全部命令的運行結果打包到一塊兒返回給client.而後此鏈接就結束事務上下文。下面能夠看一個例子
redis> multi
OK
redis> incr a
QUEUED
redis> incr b
QUEUED
redis> exec
1. (integer) 1
2. (integer) 1
從這個例子咱們能夠看到incr a ,incr b命令發出後並沒執行而是被放到了隊列中。調用exec後倆個命令被連續的執行,最後返回的是兩條命令執行後的結果
咱們能夠調用discard命令來取消一個事務。接着上面例子
redis> multi
OK
redis> incr a
QUEUED
redis> incr b
QUEUED
redis> discard
OK
redis> get a
"1"
redis> get b
"1"
能夠發現此次incr a incr b都沒被執行。discard命令其實就是清空事務的命令隊列並退出事務上下文。
雖然說redis事務在本質上也至關於序列化隔離級別的了。可是因爲事務上下文的命令只排隊並不當即執行,因此事務中的寫操做不能依賴事務中的讀操做結果。看下面例子
redis> multi
OK
redis> get a
QUEUED
redis> get b
QUEUED
redis> exec
1. "1"
2. "1"
發現問題了吧。假如咱們想用事務實現incr操做怎麼辦?能夠這樣作嗎?
redis> get a
"1"
redis> multi
OK
redis> set a 2
QUEUED
redis> exec
1. OK
redis> get a,
"2"
結論很明顯這樣是不行的。這樣和 get a 而後直接set a是沒區別的。很明顯因爲get a 和set a並不能保證兩個命令是連續執行的(get操做不在事務上下文中)。極可能有兩個client同時作這個操做。結果咱們指望是加兩次a從原來的1變成3. 可是頗有可能兩個client的get a,取到都是1,形成最終加兩次結果倒是2。主要問題咱們沒有對共享資源a的訪問進行任何的同步
也就是說redis沒提供任何的加鎖機制來同步對a的訪問。
還好redis 2.1後添加了watch命令,能夠用來實現樂觀鎖。看個正確實現incr命令的例子,只是在前面加了watch a
redis> watch a
OK
redis> get a
"1"
redis> multi
OK
redis> set a 2
QUEUED
redis> exec
1. OK
redis> get a,
"2"
watch 命令會監視給定的key,當exec時候若是監視的key從調用watch後發生過變化,則整個事務會失敗。也能夠調用watch屢次監視多個key.這樣就能夠對指定的key加樂觀鎖了。注意watch的key是對整個鏈接有效的,事務也同樣。若是鏈接斷開,監視和事務都會被自動清除。固然了 exec,discard,unwatch命令都會清除鏈接中的全部監視.
redis的事務實現是如此簡單,固然會存在一些問題。第一個問題是redis只能保證事務的每一個命令連續執行,可是若是事務中的一個命令失敗了,並不回滾其餘命令,好比使用的命令類型不匹配。
redis> set a 5
OK
redis> lpush b 5
(integer) 1
redis> set c 5
OK
redis> multi
OK
redis> incr a
QUEUED
redis> incr b
QUEUED
redis> incr c
QUEUED
redis> exec
1. (integer) 6
2. (error) ERR Operation against a key holding the wrong kind of value
3. (integer) 6
能夠看到雖然incr b失敗了,可是其餘兩個命令仍是執行了。
最後一個十分罕見的問題是當事務的執行過程當中,若是redis意外的掛了。很遺憾只有部分命令執行了,後面的也就被丟棄了。固然若是咱們使用的append-only file方式持久化,redis會用單個write操做寫入整個事務內容。便是是這種方式仍是有可能只部分寫入了事務到磁盤。發生部分寫入事務的狀況下,redis重啓時會檢測到這種狀況,而後失敗退出。可使用redis-check-aof工具進行修復,修復會刪除部分寫入的事務內容。修復完後就可以從新啓動了。
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
下面介紹詳細的快照保存過程
1.redis調用fork,如今有了子進程和父進程。
2. 父進程繼續處理client請求,子進程負責將內存內容寫入到臨時文件。因爲os的寫時複製機制(copy on write)父子進程會共享相同的物理頁面,當父進程處理寫請求時os會爲父進程要修改的頁面建立副本,而不是寫共享的頁面。因此子進程的地址空間內的數據是fork時刻整個數據庫的一個快照。
3.當子進程將快照寫入臨時文件完畢後,用臨時文件替換原來的快照文件,而後子進程退出。
client 也可使用save或者bgsave命令通知redis作一次快照持久化。save操做是在主線程中保存快照的,因爲redis是用一個主線程來處理全部 client的請求,這種方式會阻塞全部client請求。因此不推薦使用。另外一點須要注意的是,每次快照持久化都是將內存數據完整寫入到磁盤一次,並不是增量的只同步髒數據。若是數據量大的話,並且寫操做比較多,必然會引發大量的磁盤io操做,可能會嚴重影響性能。
另外因爲快照方式是在必定間隔時間作一次的,因此若是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,性能最好,持久化沒保證
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文件,這點和快照有點相似。
參照:
http://www.redis.io/
http://cocos.iteye.com/blog/1050291
http://www.cnblogs.com/liping13599168/archive/2011/04/14/2016226.html