原文:詳細介紹Redis的幾種數據結構以及使用注意事項php
《The Little Redis Book》,最好的入門小冊子,能夠先於一切文檔以前看,免費。Github上有中文翻譯,見the-little-redis-bookhtml
做者Antirez的博客,Antirez維護的Redis Twitter。java
Redis 命令中文版,huangz同窗的翻譯。mysql
Redis設計與實現,又是huangz同窗的巨做,深刻了解內部實現機制。git
Redis 2.6源碼中文註釋版,繼續是huangz同窗的大功德。github
《Redis in Action》(Manning, 2013) 挺實戰的一本書。redis
很是很是的快,有測評說比Memcached還快(當你們都是單CPU的時候),並且是無短板的快,讀寫都通常的快,全部API都差很少快,也沒有MySQL Cluster、MongoDB那樣更新同一條記錄如Counter時慢下去的毛病。算法
豐富的數據結構,超越了通常的Key-Value數據庫而被認爲是一個數據結構服務器。組合各類結構,限制Redis用途的是你本身的想象力,做者本身捉刀寫的用途入門。spring
由於是我的做品,Redis目前只有2.3萬行代碼,Keep it simple的死硬作法,使得普通公司而不需淘寶那個級別的文藝公司也能夠吃透它。
Redis宣言就是做者的自白,我最喜歡其中的"代碼像首詩","設計是一場與複雜性的戰鬥","Coding是一件艱苦的事情,惟一的辦法是享受它。若是它已不能帶來快樂就中止它。爲了防止這一天的出現,咱們要儘可能避免把Redis往乏味的路上帶。
讓人又愛又恨的單線程架構,使得代碼不用處理平時最讓人頭痛的併發而大幅簡化,也不用總是擔憂做者的併發有沒有寫對,但也帶來CPU的瓶頸,並且單線程被慢操做所阻塞時,其餘請求的延時變得不肯定。
那Redis不是什麼?
Redis 不是Big Data,數據都在內存中,沒法以T爲單位。
在Redis-Cluster發佈並被穩定使用以前,Redis沒有真正的平滑水平擴展能力。
Redis 不支持Ad-Hoc Query,提供的只是數據結構的API,沒有SQL同樣的查詢能力。
全部數據都在內存中。
五種數據結構:String / Hash / List / Set / Ordered Set。
數據過時時間支持。
不徹底的事務支持。
服務端腳本:使用Lua Script編寫,相似存儲過程的做用。
PubSub:撈過界的消息一對多發佈訂閱功能,起碼Redis-Sentinel使用了它。
持久化:支持按期導出內存的Snapshot 與 記錄寫操做日誌的Append Only File兩種模式。
Replication:Master-Slave模式,Master可鏈接多個只讀Slave,暫無專門的Geographic Replication支持。
Fail-Over:Redis-Sentinel節點負責監控Master節點,在master失效時提高slave,獨立的仲裁節點模式有效防止腦裂。
Sharding:開發中的Redis-Cluser。
動態配置:全部參數可用命令行動態配置不需重啓,並從新寫回配置文件中,對雲上的大規模部署很是合適。
做者是意大利的Salvatore Sanfilippo(antirez),又是VMWare大善人聘請了他專心寫Redis。
EMC與VMWare將旗下的開源產品如Redis和Spring都整合到了孫公司Pivotal公司。
Pivotal作的antirez訪談錄,內含一切八卦,好比他的愛好是舉重、跑步和品紅酒。
默認端口6379,是手機按鍵上MERZ對應的號碼,意大利歌女Alessia Merz是antirez和朋友們認爲愚蠢的代名詞。
Key 不能太長,好比1024字節,但antirez也不喜歡過短如"u:1000:pwd",要表達清楚意思纔好。他私人建議用":"分隔域,用"."做爲單詞間的鏈接,如"comment:1234:reply.to"。
Keys,返回匹配的key,支持通配符如 "keys a*" 、 "keys a?c",但不建議在生產環境大數據量下使用。
Sort,對集合按數字或字母順序排序後返回或另存爲list,還能夠關聯到外部key等。由於複雜度是最高的O(N+M*log(M))(N是集合大小,M 爲返回元素的數量),有時會安排到slave上執行。
Expire/ExpireAt/Persist/TTL,關於Key超時的操做。默認以秒爲單位,也有p字頭的以毫秒爲單位的版本, Redis的內部實現見2.9 過時數據清除。
最普通的key-value類型,說是String,實際上是任意的byte[],好比圖片,最大512M。 全部經常使用命令的複雜度都是O(1),普通的Get/Set方法,能夠用來作Cache,存Session,爲了簡化架構甚至能夠替換掉Memcached。
Incr/IncrBy/IncrByFloat/Decr/DecrBy,能夠用來作計數器,作自增序列。key不存在時會建立並貼心的設原值爲0。IncrByFloat專門針對float,沒有對應的decrByFloat版本?用負數啊。
SetNx, 僅當key不存在時才Set。能夠用來選舉Master或作分佈式鎖:全部Client不斷嘗試使用SetNx master myName搶注Master,成功的那位不斷使用Expire刷新它的過時時間。
若是Master倒掉了key就會失效,剩下的節點又會發生新一輪搶奪。
其餘Set指令:
SetEx, Set + Expire 的簡便寫法,p字頭版本以毫秒爲單位。
GetSet, 設置新值,返回舊值。好比一個按小時計算的計數器,能夠用GetSet獲取計數並重置爲0。這種指令在服務端作起來是舉手之勞,客戶端便方便不少。
MGet/MSet/MSetNx, 一次get/set多個key。
2.6.12版開始,Set命令已融合了Set/SetNx/SetEx三者,SetNx與SetEx可能會被廢棄,這對Master搶注很是有用,不用擔憂setNx成功後,來不及執行Expire就倒掉了。惋惜有些懶惰的Client並無快速支持這個新指令。
GetBit/SetBit/BitOp,與或非/BitCount, BitMap的玩法,好比統計今天的獨立訪問用戶數時,每一個註冊用戶都有一個offset,他今天進來的話就把他那個位設爲1,用BitCount就能夠得出今天的總人樹。
Append/SetRange/GetRange/StrLen,對文本進行擴展、替換、截取和求長度,只對特定數據格式如字段定長的有用,json就沒什麼用。
Key-HashMap結構,相比String類型將這整個對象持久化成JSON格式,Hash將對象的各個屬性存入Map裏,能夠只讀取/更新對象的某些屬性。
這樣有些屬性超長就讓它一邊呆着不動,另外不一樣的模塊能夠只更新本身關心的屬性而不會互相併發覆蓋衝突。
另外一個用法是土法建索引。好比User對象,除了id有時還要按name來查詢。
能夠有以下的數據記錄:
(String) user:101 -> {"id":101,"name":"calvin"...} (String) user:102 -> {"id":102,"name":"kevin"...} (Hash) user:index-> "calvin"->101, "kevin" -> 102
底層實現是hash table,通常操做複雜度是O(1),要同時操做多個field時就是O(N),N是field的數量。
List是一個雙向鏈表,支持雙向的Pop/Push,江湖規矩通常從左端Push,右端Pop——LPush/RPop,並且還有Blocking的版本BLPop/BRPop,客戶端能夠阻塞在那直到有消息到來,全部操做都是O(1)的好孩子,能夠當Message Queue來用。
當多個Client併發阻塞等待,有消息入列時誰先被阻塞誰先被服務。任務隊列系統Resque是其典型應用。
還有RPopLPush/ BRPopLPush,彈出來返回給client的同時,把本身又推入另外一個list,LLen獲取列表的長度。
還有按值進行的操做:LRem(按值刪除元素)、LInsert(插在某個值的元素的先後),複雜度是O(N),N是List長度,由於List的值不惟一,因此要遍歷所有元素,而Set只要O(log(N))。
按下標進行的操做:下標從0開始,隊列從左到右算,下標爲負數時則從右到左。
LSet ,按下標設置元素值。
LIndex,按下標返回元素。
LRange,不一樣於POP直接彈走元素,只是返回列表內一段下標的元素,是分頁的最愛。
LTrim,限制List的大小,好比只保留最新的20條消息。
複雜度也是O(N),其中LSet的N是List長度,LIndex的N是下標的值,LRange的N是start的值+列出元素的個數,由於是鏈表而不是數組,因此按下標訪問其實要遍歷鏈表,除非下標正好是隊頭和隊尾。LTrim的N是移除元素的個數。
在消息隊列中,並無JMS的ack機制,若是消費者把job給Pop走了又沒處理完就死機了怎麼辦?
解決方法之一是加多一個sorted set,分發的時候同時發到list與sorted set,以分發時間爲score,用戶把job作完了以後要用ZREM消掉sorted set裏的job,而且定時從sorted set中取出超時沒有完成的任務,從新放回list。
另外一個作法是爲每一個worker多加一個的list,彈出任務時改用RPopLPush,將job同時放到worker本身的list中,完成時用LREM消掉。
若是集羣管理(如zookeeper)發現worker已經掛掉,就將worker的list內容從新放回主list。
Set就是Set,能夠將重複的元素隨便放入而Set會自動去重,底層實現也是hash table。
SAdd/SRem/SIsMember/SCard/SMove/SMembers,各類標準操做。除了SMembers都是O(1)。
SInter/SInterStore/SUnion/SUnionStore/SDiff/SDiffStore,各類集合操做。交集運算能夠用來顯示在線好友(在線用戶 交集 好友列表),共同關注(兩個用戶的關注列表的交集)。
O(N),並集和差集的N是集合大小之和,交集的N是小的那個集合的大小*2。
有序集,元素放入集合時還要提供該元素的分數。
ZRange/ZRevRange,按排名的上下限返回元素,正數與倒數。
ZRangeByScore/ZRevRangeByScore,按分數的上下限返回元素,正數與倒數。
ZRemRangeByRank/ZRemRangeByScore,按排名/按分數的上下限刪除元素。
ZCount,統計分數上下限之間的元素個數。
ZRank/ZRevRank ,顯示某個元素的正倒序的排名。
ZScore/ZIncrby,顯示元素的分數/增長元素的分數。
ZAdd(Add)/ZRem(Remove)/ZCard(Count),ZInsertStore(交集)/ZUnionStore(並集),Set操做,與正牌Set相比,少了IsMember和差集運算。
Sorted Set的實現是hash table(element->score, 用於實現ZScore及判斷element是否在集合內),和skip list(score->element,按score排序)的混合體。
skip list有點像平衡二叉樹那樣,不一樣範圍的score被分紅一層一層,每層是一個按score排序的鏈表。
ZAdd/ZRem是O(log(N)),ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是結果/操做元素的個數。
可見,本來可能很大的N被很關鍵的Log了一下,1000萬大小的Set,複雜度也只是幾十不到。
固然,若是一次命中不少元素M很大那誰也沒辦法了。
用Multi(Start Transaction)、Exec(Commit)、Discard(Rollback)實現。
在事務提交前,不會執行任何指令,只會把它們存到一個隊列裏,不影響其餘客戶端的操做。在事務提交時,批量執行全部指令。《Redis設計與實現》中的詳述。
注意,Redis裏的事務,與咱們平時的事務概念很不同:
它僅僅是保證事務裏的操做會被連續獨佔的執行。由於是單線程架構,在執行完事務內全部指令前是不可能再去同時執行其餘客戶端的請求的。
它沒有隔離級別的概念,由於事務提交前任何指令都不會被實際執行,也就不存在"事務內的查詢要看到事務裏的更新,在事務外查詢不能看到"這個讓人萬分頭痛的問題。
它不保證原子性——全部指令同時成功或同時失敗,只有決定是否開始執行所有指令的能力,沒有執行到一半進行回滾的能力。
在redis裏失敗分兩種,一種是明顯的指令錯誤,好比指令名拼錯,指令參數個數不對,在2.6版中所有指令都不會執行。
另外一種是隱含的,好比在事務裏,第一句是SET foo bar, 第二句是LLEN foo,對第一句產生的String類型的key執行LLEN會失敗,但這種錯誤只有在指令運行後才能發現,這時候第一句成功,第二句失敗。
還有,若是事務執行到一半redis被KILL,已經執行的指令一樣也不會被回滾。
Watch指令,相似樂觀鎖,事務提交時,若是Key的值已被別的客戶端改變,好比某個list已被別的客戶端push/pop過了,整個事務隊列都不會被執行。
Redis2.6內置的Lua Script支持,能夠在Redis的Server端一次過運行大量邏輯,就像存儲過程同樣,避免了海量中間數據在網路上的傳輸。
Lua自稱是在Script語言裏關於快的標準,Redis選擇了它而不是流行的JavaScript。
由於Redis的單線程架構,整個Script默認是在一個事務裏的。
Script裏涉及的全部Key儘可能用變量,從外面傳入,使Redis一開始就知道你要改變哪些key,爲了往後作水平分區作準備。若是涉及的key在不一樣服務器......
Eval每次傳輸一整段Script比較費帶寬,能夠先用Script Load載入script,返回哈希值。而後用EvalHash執行。由於就是SHA-1,因此任什麼時候候執行返回的哈希值都是同樣的。
內置的Lua庫裏還很貼心的帶了CJSON,能夠處理json字符串。
Script一旦執行則不容易中斷,中斷了也會有不可知後果,所以最好在開發環境充分測試了再上線。
一段用Redis作Timer的示例代碼,下面的script被按期調用,從以觸發時間爲score的sorted set中取出已到期的Job,放到list中給Client們blocking popup。
-- KEYS: [1]job:sleeping, [2]job:ready -- ARGS: [1]currentTime -- Comments: result is the job id local jobs=redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1]) local count = table.maxn(jobs) if count>0 then -- Comments: remove from Sleeping Job sorted set redis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1]) -- Comments: add to the Ready Job list -- Comments: can optimize to use lpush id1,id2,... for better performance for i=1,count do redis.call('lpush', KEYS[2], jobs[i]) end end
官方文檔 與 《Redis設計與實現》中的詳述,過時數據的清除歷來不容易,爲每一條key設置一個timer,到點馬上刪除的消耗太大,每秒遍歷全部數據消耗也大,Redis使用了一種相對務實的作法:
當client主動訪問key會先對key進行超時判斷,過期的key會馬上刪除。
若是clien永遠都再也不get那條key呢? 它會在Master的後臺,每秒10次的執行以下操做: 隨機選取100個key校驗是否過時,若是有25個以上的key過時了,馬上額外隨機選取下100個key(不計算在10次以內)。
可見,若是過時的key很少,它最多每秒回收200條左右,若是有超過25%的key過時了,它就會作得更多,但只要key不被主動get,它佔用的內存何時最終被清理掉只有天知道。
測試環境: RHEL 6.3 / HP Gen8 Server/ 2 * Intel Xeon 2.00GHz(6 core) / 64G DDR3 memory / 300G RAID-1 SATA / 1 master(writ AOF), 1 slave(write AOF & RDB)
數據準備: 預加載兩千萬條數據,佔用10G內存。
測試工具:自帶的redis-benchmark,默認只是基於一個很小的數據集進行測試,調整命令行參數以下,就能夠開100條線程(默認50),SET 1千萬次(key在0-1千萬間隨機),key長21字節,value長256字節的數據。
redis-benchmark -t SET -c 100 -n 10000000 -r 10000000 -d 256
測試結果(TPS): 1.SET:4.5萬, 2.GET:6萬 ,3.INCR:6萬,4.真實混合場景: 2.5萬SET & 3萬GET
單條客戶端線程時6千TPS,50與100條客戶端線程差異不大,200條時會略多。
Get/Set操做,通過了LAN,延時也只有1毫秒左右,能夠反覆放心調用,不用像調用REST接口和訪問數據庫那樣,每多一次外部訪問都心痛。
資源監控:
1.CPU: 佔了一個處理器的100%,總CPU是4%(由於總共有2CPU*6核*超線程 = 24個處理器),可見單線程下單處理器的能力是瓶頸。 AOF rewrite時另外一個處理器佔用50-70%。
2.網卡:15-20 MB/s receive, 3Mb/s send(no slave) or 15-20 MB/s send (with slave) 。當把value長度加到4K時,receive 99MB/s,已經到達千兆網卡的瓶頸,TPS降到2萬。
3.硬盤:15MB/s(AOF append), 100MB/s(AOF rewrite/AOF load,普通硬盤的瓶頸)
純ANSI C編寫。
不依賴第三方類庫,沒有像memcached那樣使用libevent,由於libevent迎合通用性而形成代碼龐大,因此做者用libevent中兩個文件修改實現了本身的epoll event loop。微軟的兼容Windows補丁也由於一樣緣由被拒了。
快,緣由之一是Redis多樣的數據結構,每種結構只作本身愛作的事,固然比數據庫只有Table,MongogoDB只有JSON一種結構快了。
惋惜單線程架構,雖然做者認爲CPU不是瓶頸,內存與網絡帶寬纔是。但實際測試時並不是如此,見上。
官方文檔關於各類產生Latency的緣由的詳細分析, 中文版
正視網絡往返時間:
1.MSet/LPush/ZAdd等都支持一次輸入多個Key。
2.PipeLining模式 能夠一次輸入多個指令。
3.更快的是Lua Script模式,還能夠包含邏輯,直接在服務端又get又set的,見2.8 Lua Script。
發現執行緩慢的命令,可配置執行超過多少時間的指令算是緩慢指令(默認10毫秒,不含IO時間),能夠用slowlog get 指令查看(默認只保留最後的128條)。
單線程的模型下,一個請求佔掉10毫秒是件大事情,注意設置和顯示的單位爲微秒。
CPU永遠是瓶頸,但top看到單個CPU 100%時,就是垂直擴展的時候了。
持久化對性能的影響很大,見5.1持久化。
要熟悉各指令的複雜度,不過只要不是O(N)一個超大集合,都不用太擔憂。
全部的數據都必須在內存中,原來2.0版的VM策略(將Value放到磁盤,Key仍然放在內存),2.4版後嫌麻煩又不支持了。
必定要設置最大內存,不然物理內存用爆了就會大量使用Swap,寫RDB文件時的速度慢得你想死。
多留一倍內存是最安全的。重寫AOF文件和RDB文件的進程(即便不作持久化,複製到Slave的時候也要寫RDB)會fork出一條新進程來,採用了操做系統的Copy-On-Write策略(子進程與父進程共享Page。
若是父進程的Page-每頁4K有修改,父進程本身建立那個Page的副本,不會影響到子進程,父愛如山)。留意Console打出來的報告,如"RDB: 1215 MB of memory used by copy-on-write"。
在系統極度繁忙時,若是父進程的全部Page在子進程寫RDB過程當中都被修改過了,就須要兩倍內存。
按照Redis啓動時的提醒,設置 vm.overcommit_memory = 1 ,使得fork()一條10G的進程時,由於COW策略而不必定須要有10G的free memory。
其餘須要考慮的內存包括:
1.AOF rewrite過程當中對新寫入命令的緩存(rewrite結束後會merge到新的aof文件),留意"Background AOF buffer size: 80 MB"的字樣。
2.負責與Slave同步的Client的緩存,默認設置master須要爲每一個slave預留不高於256M的緩存(見5.1持久化)。
當最大內存到達時,按照配置的Policy進行處理, 默認策略爲volatile-lru,對設置了expire time的key進行LRU清除(不是按實際expire time)。
若是沒有數據設置了expire time或者policy爲noeviction,則直接報錯,但此時系統仍支持get之類的讀操做。
另外還有幾種policy,好比volatile-ttl按最接近expire time的,allkeys-lru對全部key都作LRU。
測試代表,string類型須要90字節的額外代價,就是說key 1個字節,value 1個字節時,仍是須要佔用92字節的長度,而上面的benchmark的記錄就佔用了367個字節。
其餘類型可根據文檔自行計算或實際測試一下。
使用jemalloc分配內存,刪除數據後,內存並不會乖乖還給操做系統而是被Redis截留下來重用到新的數據上,直到Redis重啓。
所以進程實際佔用內存是看INFO裏返回的used_memory_peak_human。
Redis內部用了ziplist/intset這樣的壓縮結構來減小hash/list/set/zset的存儲,默認當集合的元素少於512個且最長那個值不超過64字節時使用,可配置。
用make 32bit能夠編譯出32位的版本,每一個指針佔用的內存更小,但只支持最大4GB內存。
其實,大內存加上垂直分區也夠了,不必定非要沙丁一把。
Jedis支持在客戶端作分區,侷限是不能動態re-sharding, 有分區的master倒了,不能減小分區必須用slave頂上。要增長分區的話,呃.....
antire在博客裏提到了Twemproxy,一個Twitter寫的Proxy,但它在發現節點倒掉後,只會從新計算一致性哈希環,把數據存到別的master去,而不是集成Sentinel指向新由slave升級的master,像Memcached同樣的作法也只適合作Cache的場景。
Redis-Cluster是今年工做重點,支持automatic re-sharding, 採用和Hazelcast相似的算法,總共有N個分區(eg.N=1024),每臺Server負責若干個分區。
在客戶端先hash出key 屬於哪一個分區,隨便發給一臺server,server會告訴它真正哪一個Server負責這個分區,緩存下來,下次還有該分區的請求就直接發到地兒了。
Re-sharding時,會將某些分區的數據移到新的Server上,完成後各Server周知分區<->Server映射的變化,由於分區數量有限,因此通信量不大。
在遷移過程當中,客戶端緩存的依然是舊的分區映射信息,原server對於已經遷移走的數據的get請求,會返回一個臨時轉向的應答,客戶端先不會更新Cache。
等遷移完成了,就會像前面那樣返回一條永久轉向信息,客戶端更新Cache,之後就都去新server了。
高可用性關乎系統出錯時到底會丟失多少數據,多久不能服務。要綜合考慮持久化,Master-Slave複製及Fail-Over配置,以及具體Crash情形,好比Master死了,但Slave沒死。或者只是Redis死了,操做系統沒死等等。
綜述: 解密Redis持久化(中文歸納版), 英文原版,《Redis設計與實現》: RDB 與 AOF。
不少人開始會想象二者是互相結合的,即dump出一個snapshot到RDB文件,而後在此基礎上記錄變化日誌到AOF文件。
實際上二者毫無關係,徹底獨立運行,由於做者認爲簡單纔不會出錯。若是使用了AOF,重啓時只會從AOF文件載入數據,不會再管RDB文件。
正確關閉服務器:redis-cli shutdown 或者 kill,都會graceful shutdown,保證寫RDB文件以及將AOF文件fsync到磁盤,不會丟失數據。
若是是粗暴的Ctrl+C,或者kill -9 就可能丟失。
RDB是整個內存的壓縮過的Snapshot,RDB的數據結構,能夠配置複合的快照觸發條件,默認是1分鐘內改了1萬次,或5分鐘內改了10次,或15分鐘內改了1次。
RDB寫入時,會連內存一塊兒Fork出一個新進程,遍歷新進程內存中的數據寫文件,這樣就解決了些Snapshot過程當中又有新的寫入請求進來的問題。 Fork的細節見4.1最大內存。
RDB會先寫到臨時文件,完了再Rename成,這樣外部程序對RDB文件的備份和傳輸過程是安全的。並且即便寫新快照的過程當中Server被強制關掉了,舊的RDB文件還在。
可配置是否進行壓縮,壓縮方法是字符串的LZF算法,以及將string形式的數字變回int形式存儲。
動態全部中止RDB保存規則的方法:redis-cli config set save ""
操做日誌,記錄全部有效的寫操做,等於mysql的binlog,格式就是明文的Redis協議的純文本文件。
通常配置成每秒調用一次fdatasync將kernel的文件緩存刷到磁盤。當操做系統非正常關機時,文件可能會丟失不超過2秒的數據(更嚴謹的定義見後)。 若是設爲fsync always,性能只剩幾百TPS,不用考慮。
若是設爲no,靠操做系統本身的sync,Linux系統通常30秒一次。
AOF文件持續增加而過大時,會fork出一條新進程來將文件重寫(也是先寫臨時文件,最後再rename,), 遍歷新進程的內存中數據,每條記錄有一條的Set語句。
默認配置是當AOF文件大小是上次rewrite後大小的一倍,且文件大於64M時觸發。
Redis協議,如set mykey hello,將持久化成*3 $3 set $5 mykey $5 hello, 第一個數字表明這條語句有多少元,其餘的數字表明後面字符串的長度。
這樣的設計,使得即便在寫文件過程當中忽然關機致使文件不完整,也能自我修復,執行redis-check-aof便可。
綜上所述,RDB的數據不實時,同時使用二者時服務器重啓也只會找AOF文件。
那要不要只使用AOF呢?做者建議不要,由於RDB更適合用於備份數據庫(AOF在不斷變化很差備份),快速重啓,並且不會有AOF可能潛在的bug,留着做爲一個萬一的手段。
AOF重寫和RDB寫入都是在fork出新進程後,遍歷新進程的內存順序寫的,既不阻塞主進程繼續處理客戶端請求,順序寫的速度也比隨機寫快。
測試把剛纔benchmark的11G數據寫成一個1.3的RDB文件,或者等大的AOF文件rewrite,須要80秒,在redis-cli info中可查看。啓動時載入一個AOF或RDB文件的速度與上面寫入時相同,在log中可查看。
Fork一個使用了大量內存的進程也要時間,大約10ms per GB的樣子,但Xen在EC2上是讓人鬱悶的239ms (KVM和VMWare貌似沒有這個毛病),各類系統的對比,Info指令裏的latest_fork_usec顯示上次花費的時間。
在bgrewriteaof過程當中,全部新來的寫入請求依然會被寫入舊的AOF文件,同時放到buffer中,當rewrite完成後,會在主線程把這部份內容合併到臨時文件中以後才rename成新的AOF文件。
因此rewrite過程當中會不斷打印"Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB",計算系統容量時要留意這部分的內存消耗。
注意,這個合併的過程是阻塞的,若是你產生了280MB的buffer,在100MB/s的傳統硬盤上,Redis就要阻塞2.8秒!!!
NFS或者Amazon上的EBS都不推薦,由於它們也要消耗帶寬。
bgsave和bgaofrewrite不會被同時執行,若是bgsave正在執行,bgaofrewrite會自動延後。
2.4版之後,寫入AOF時的fdatasync由另外一條線程來執行,不會再阻塞主線程。
2.4版之後,lpush/zadd能夠輸入一次多個值了,使得AOF重寫時能夠將舊版本中的多個lpush/zadd指令合成一個,每64個key串一串。
由於RDB文件只用做後備用途,建議只在Slave上持久化RDB文件,並且只要15分鐘備份一次就夠了,只保留save 900 1這條規則。
若是Enalbe AOF,好處是在最惡劣狀況下也只會丟失不超過兩秒數據,啓動腳本較簡單隻load本身的AOF文件就能夠了。
代價一是帶來了持續的IO,二是AOF rewrite的最後將rewrite過程當中產生的新數據寫到新文件形成的阻塞幾乎是不可避免的。
只要硬盤許可,應該儘可能減小AOF rewrite的頻率,AOF重寫的基礎大小默認值64M過小了,能夠設到5G以上。
默認超過原大小100%大小時重寫能夠改到適當的數值,好比以前的benchmark每一個小時會產生40G大小的AOF文件,若是硬盤能撐到半夜系統閒時才用cron調度bgaofrewrite就行了。
若是不Enable AOF,僅靠Master-Slave Replication 實現高可用性也能夠。能省掉一大筆IO也減小了rewrite時帶來的系統波動。
代價是若是Master/Slave同時倒掉,會丟失十幾分鐘的數據,啓動腳本也要比較兩個Master/Slave中的RDB文件,載入較新的那個。新浪微博就選用了這種架構,見Tim的博客
當AOF rewrite 15G大小的內存時,Redis整個死掉的樣子,全部指令甚至包括slave發到master的ping,redis-cli info都不能被執行。
官方文檔,由IO產生的Latency詳細分析, 已經預言了悲劇的發生,但一開始沒留意。
Redis爲求簡單,採用了單請求處理線程結構。
打開AOF持久化功能後, Redis處理完每一個事件後會調用write(2)將變化寫入kernel的buffer,若是此時write(2)被阻塞,Redis就不能處理下一個事件。
Linux規定執行write(2)時,若是對同一個文件正在執行fdatasync(2)將kernel buffer寫入物理磁盤,或者有system wide sync在執行,write(2)會被block住,整個Redis被block住。
若是系統IO繁忙,好比有別的應用在寫盤,或者Redis本身在AOF rewrite或RDB snapshot(雖然此時寫入的是另外一個臨時文件,雖然各自都在連續寫,但兩個文件間的切換使得磁盤磁頭的尋道時間加長),就可能致使fdatasync(2)遲遲未能完成從而block住write(2),block住整個Redis。
爲了更清晰的看到fdatasync(2)的執行時長,可使用"strace -p (pid of redis server) -T -e -f trace=fdatasync",但會影響系統性能。
Redis提供了一個自救的方式,當發現文件有在執行fdatasync(2)時,就先不調用write(2),只存在cache裏,省得被block。但若是已經超過兩秒都仍是這個樣子,則會硬着頭皮執行write(2),即便redis會被block住。
此時那句要命的log會打印:「Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.」
以後用redis-cli INFO能夠看到aof_delayed_fsync的值被加1。
所以,對於fsync設爲everysec時丟失數據的可能性的最嚴謹說法是:若是有fdatasync在長時間的執行,此時redis意外關閉會形成文件裏很少於兩秒的數據丟失。
若是fdatasync運行正常,redis意外關閉沒有影響,只有當操做系統crash時纔會形成少於1秒的數據丟失。
最後發現,原來是AOF rewrite時一直埋頭的調用write(2),由系統本身去觸發sync。在RedHat Enterprise 6裏,默認配置vm.dirty_background_ratio=10,也就是佔用了10%的可用內存纔會開始後臺flush,而個人服務器有64G內存。
很明顯一次flush太多數據會形成阻塞,因此最後果斷設置了sysctl vm.dirty_bytes=33554432(32M),問題解決。
而後提了個issue,AOF rewrite時定時也執行一下fdatasync嘛, antirez三分鐘後就回復了,新版中,AOF rewrite時32M就會重寫主動調用fdatasync。
slave能夠在配置文件、啓動命令行參數、以及redis-cli執行SlaveOf指令來設置本身是奴隸。
測試代表同步延時很是小,指令一旦執行完畢就會馬上寫AOF文件和向Slave轉發,除非Slave本身被阻塞住了。
比較蠢的是,即便在配置文件裏設了slavof,slave啓動時依然會先從數據文件載入一堆沒用的數據,再去執行slaveof。
"Slaveof no one",立馬變身master。
2.8版本將支持PSYNC部分同步,master會撥出一小段內存來存放要發給slave的指令,若是slave短暫的斷開了,重連時會從內存中讀取須要補讀的指令,這樣就不須要斷開兩秒也搞一次全同步了。
但若是斷開時間較長,已經超過了內存中保存的數據,就仍是要全同步。
Slave也能夠接收Read-Only的請求。
先執行一次全同步 -- 請求master BgSave出本身的一個RDB Snapshot文件發給slave,slave接收完畢後,清除掉本身的舊數據,而後將RDB載入內存。
再進行增量同步 -- master做爲一個普通的client連入slave,將全部寫操做轉發給slave,沒有特殊的同步協議。
有時候明明master/slave都活得好好的,忽然間就說要從新進行全同步了:
1)Slave顯示:
# MASTER time out: no data nor PING received...
slave會每隔repl-ping-slave-period(默認10秒)ping一次master,若是超過repl-timeout(默認60秒)都沒有收到響應,就會認爲Master掛了。
若是Master明明沒掛但被阻塞住了也會報這個錯。能夠適當調大repl-timeout。
2)Master顯示:
# Client addr=10.175.162.123:44670 flags=S oll=104654 omem=2147487792 events=rw cmd=sync scheduled to be closed ASAP for overcoming of output buffer limits.
當slave沒掛但被阻塞住了,好比正在loading Master發過來的RDB, Master的指令不能馬上發送給slave,就會放在output buffer中(見oll是命令數量,omem是大小),
在配置文件中有以下配置:client-output-buffer-limit slave 256mb 64mb 60, 這是說負責發數據給slave的client,若是buffer超過256m或者連續60秒超過64m,就會被馬上強行關閉!!! Traffic大的話必定要設大一點。
不然就會出現一個很悲劇的循環,Master傳輸一個大的RDB給Slave,Slave努力的裝載,但還沒裝載完,Master對client的緩存滿了,再來一次。
平時能夠在master執行 redis-cli client list 找那個cmd=sync,flag=S的client,注意OMem的變化。
Redis-sentinel是2.6版開始加入的另外一組獨立運行的節點,提供自動Fail Over的支持。
官方文檔 與 Redis核心解讀–集羣管理工具(Redis-sentinel)
Sentinel每秒鐘對全部master,slave和其餘sentinel執行Ping,redis-server節點要應答+PONG或-LOADING或-MASTERDOWN.
若是某一臺Sentinel沒有在30秒內(可配置得短一些哦)收到上述正確應答,它就會認爲master處於sdown狀態(主觀Down)
它向其餘sentinel詢問是否也認爲該master倒了(SENTINEL is-master-down-by-addr ), 若是quonum臺(默認是2)sentinel在5秒鐘內都這樣認爲,就會認爲master真是odown了(客觀Down)。
此時會選出一臺sentinel做爲Leader執行fail-over, Leader會從slave中選出一個提高爲master(執行slaveof no one),而後讓其餘slave指向它(執行slaveof new master)。
master地址在sentinel.conf裏, sentinel會每10秒一次向master發送INFO,知道master的slave有哪些。 若是master已經變爲slave,sentinel會分析INFO的應答指向新的master。
之前,sentinel重啓時,若是master已經切換過了,但sentinel.conf裏master的地址並無變,極可能有悲劇發生。
另外master重啓後若是沒有切換成slave,也可能有悲劇發生。新版好像修復了一點這個問題,待研究。
另外,sentinel會在master上建一個pub/sub channel,名爲"sentinel:hello",通告各類信息,sentinel們也是經過接收pub/sub channel上的+sentinel的信息發現彼此,由於每臺sentinel每5秒會發送一次本身的host信息,宣告本身的存在。
sentinel在failover時還會執行配置文件裏指定的用戶自定義reconfig腳本,作用戶本身想作的事情,好比讓master變爲slave並指向新的master。
腳本的將會在命令行按順序傳入以下參數: <master-name> <role(leader/observer)> <state(上述三種狀況)> <from-ip> <from-port> <to-ip> <to-port>
腳本返回0是正常,若是返回1會被從新執行,若是返回2或以上不會。 若是超過60秒沒返回會被強制終止。
以爲Sentinel至少有兩個可提高的地方:
一是若是master 主動shutdown,好比系統升級,有辦法主動通知sentinel提高新的master,減小服務中斷時間。
二是比起redis-server太原始了,要本身醜陋的以nohup sentinel > logfile 2>&1 & 啓動,也不支持shutdown命令,要本身kill pid。
基於Sentinel的方案,client須要執行語句SENTINEL get-master-addr-by-name mymaster 可得到當前master的地址。
Jedis正在集成sentinel,已經支持了sentinel的一些指令,但還沒發佈,但sentinel版的鏈接池則暫時徹底沒有,在公司的項目裏我參考網友的項目本身寫了一個。
淘寶的Tedis driver,使用了徹底不一樣的思路,不基於Sentinel,而是多寫隨機讀, 一開始就同步寫入到全部節點,讀的話隨便讀一個還活着的節點就好了。
但有些節點成功有些節點失敗如何處理? 節點死掉從新起來後怎麼從新同步?何時能夠從新Ready? 因此不是很敢用。
另外如Ruby寫的redis_failover,也是拋開了Redis Sentinel,基於ZooKeeper的臨時方案。
Redis做者也在博客裏抱怨怎麼沒有人作Dynamo-style 的client。
沒有特別支持,依然用Master Slave複製,3Scale想出了諸如用壓縮的SSH隧道下降傳輸量等方法。
安裝包製做:沒有現成,須要本身編譯,本身寫rpm包的腳本,可參考utils中的install_server.sh與redis_init_script。
但RHEL下設定script runlevel的方式不同,redis_init_script中要增長一句 "# chkconfig: 345 90 10" ,而install_server.sh能夠刪掉後面的那句「chkconfig --level 345 reis"
雲服務:Redis Cloud,在Amazon、Heroku、Windows Azure、App Frog上提供雲服務,供一樣部署在這些雲上的應用使用。
其餘的雲服務有GarantiaData,已被redis-cloud收購。另外還有Redis To Go, OpenRedis, RedisGreen。
CopperEgg統計本身的用戶在AWS上的數據庫部署:mysqld佔了50%半壁江山, redis佔了18%排第二, mongodb也有11%, cassandra是3%,Oracle只有可憐的2%。
Chef Recipes:brianbianco/redisio,活躍,同步更新版本。
Redis只能使用單線程,爲了提升CPU利用率,有提議在同一臺服務器上啓動多個Redis實例,但這會帶來嚴重的IO爭用,除非Redis不須要持久化,或者有某種方式保證多個實例不會在同一個時間重寫AOF。
一組sentinel能同時監控多個Master。
有提議說環形的slave結構,即master只連一個slave,而後slave再連slave,此部署有兩個前提,一是有大量的只讀需求須要在slave完成,二是對slave傳遞時的數據不一致性不敏感。
約30個配置項,全都有默認配置,對redif.conf默認配置的修改見附錄1。
能夠配置文件中編寫。
能夠在啓動時的命令行配置,redis-server --port 7777 --slaveof 127.0.0.1 8888。
雲時代大規模部署,把配置文件滿街傳顯然不是好的作法, 能夠用redis-cli執行Config Set指令, 修改全部的參數,達到維護人員最愛的不重啓服務而修改參數的效果,並且在新版本里還能夠執行 Config Rewrite 將改動寫回到文件中,不過所有默認值都會打印出來,可能會破壞掉原來的文件的排版,註釋。
在配置文件裏設置密碼:requirepass foobar。
禁止某些危險命令,好比殘暴的FlushDB,將它rename成"":rename-command FLUSHDB ""。
綜述: Redis監控技巧
Info指令將返回很是豐富的信息。 着重監控檢查內存使用,是否已接近上限,used_memory是Redis申請的內存,used_memory_rss是操做系統分配給Redis的物理內存,二者之間隔着碎片,隔着Swap。
還有重點監控 AOF與RDB文件的保存狀況,以及master-slave的關係。Statistic 信息還包括key命中率,全部命令的執行次數,全部client鏈接數量等, CONFIG RESETSTAT 可重置爲0。
Monitor指令能夠顯示Server收到的全部指令,主要用於debug,影響性能,生產環境慎用。
SlowLog 檢查慢操做(見2.性能)。
日誌能夠動態的設置成verbose/debug模式,但不見得有更多有用的log可看,verbose還會很煩的每5秒打印當前的key狀況和client狀況。指令爲config set loglevel verbose。
最愛Redis的地方是代碼只有2.3萬行,並且編碼優美,並且huangz同窗還在原來的註釋上再加上了中文註釋——Redis 2.6源碼中文註釋版 ,因此雖然是C寫的代碼,雖然有十年沒看過C代碼,但這幾天trouble shooting毫無難度,一看就懂。
Trobule shotting的經歷證實antirez處理issue的速度很是快(若是你的issue言之有物的話),比Weblogic之類的商業支持還好。
若是AOF文件在寫入過程當中crash,能夠用redis-check-aof修復,見5.1.2
若是AOF rewrite和 RDB snapshot的過程當中crash,會留下無用的臨時文件,須要按期掃描刪除。
官網列出了以下工具,但暫時沒發現會直接拿來用的:
Redis Live,基於Python的web應用,使用Info和Monitor得到系統狀況和指令統計分析。 由於Monitor指令影響性能,因此建議用cron按期運行,每次偷偷採樣兩分鐘的樣子。
phpRedisAdmin,基於php的Web應用,目標是MysqlAdmin那樣的管理工具,能夠管理每一條Key的狀況,但它的界面應該只適用於Key的數量不太多的狀況,Demo。
Redis Faina,基於Python的命令行,Instagram出品,用戶自行得到Monitor的輸出後發給它進行統計分析。因爲Monitor輸出的格式在Redis版本間不同,要去github下最新版。
Redis-rdb-tools 基於Python的命令行,能夠分析RDB文件每條Key對應value所佔的大小,還能夠將RDB dump成普通文本文件而後比較兩個庫是否一致,還能夠將RDB輸出成JSON格式,多是最有用的一個了。
Redis Sampler,基於Ruby的命令行,antirez本身寫的,統計數據分佈狀況。
各個Driver好像只有Jedis比較活躍,但也5個月沒提交了,也是Java裏惟一的Redis官方推薦。
Spring Data Redis的封裝並不太必要,由於Jedis已足夠簡單,沒有像Spring Data MongoDB對MongoDB java driver的封裝那樣大幅簡化代碼,頂多就是增強了一點點點pipeline和transaction狀態下的coding,禁止了一些此狀態下不能用的命令。
而所謂屏蔽各類底層driver的差別並不太吸引人,由於我就沒打算選其餘幾種driver。有興趣的能夠翻翻它的JedisConnection代碼。
因此,SpringSide直接在Jedis的基礎上,按Spring的風格封裝了一個JedisTemplate,負責從池中獲取與歸還Jedis實例,處理異常。
Jedis基於Apache Commons Pool作的鏈接池,默認MaxActive最大鏈接數只有8,必須從新設置。並且MaxIdle也要相應增大,不然全部新建的鏈接用完即棄,而後會不停的從新鏈接。
另外Jedis設定了每30秒對全部鏈接執行一次ping,以發現失效的鏈接,這樣每30秒會有一個拿不到鏈接的高峯。
但效果如何須要獨立分析。好比系統高峯以後可能有一長段時間很閒,並且Redis Server那邊作了Timeout控制會把鏈接斷掉,這時候作idle checking是有意義的,但30秒一次也太過頻繁了。不然關掉它更好。
Jedis的blocking pop函數,應用執行ExecutorService.shutdownNow()中斷線程時並不能把它中斷,見討論組。
兩個解決方法:
不要用不限時的blocking popup,傳多一個超時時間參數,如5秒。
找地方將調用blocking popup的jedis保存起來,shutdown時主動調用它的close。
Redis默認最大鏈接數是一萬。
Redis默認不對Client作Timeout處理,能夠用timeout 項配置,但即便配了也不會很是精確。
Windows版本方便對應用的本地開發調試,但Redis並無提供,好在微軟提供了一個依賴LibUV實現兼容的補丁,https://github.com/MSOpenTech/redis,但redis做者拒絕合併到master中,微軟只好苦憋的時時人工同步。
目前的穩定版是2.6版本,支持Lua腳本。
由於github如今已經沒有Download服務了,因此編譯好的可執行文件藏在這裏:
https://github.com/MSOpenTech/redis/tree/2.6/bin/release
注:下文中的連接都是網站的架構描述文檔。
Twitter和新浪微博, 都屬於將Redis各類數據結構用得出神入化的那種,如何發佈大V如奧巴馬的消息是它們最頭痛的問題。
Tumblr: 11億美刀賣給Yahoo的圖片日誌網站,22 臺Redis server,每臺運行8 - 32個實例,總共100多個Redis實例在跑。
有着Redis has been completely problem free and the community is great的崇高評價。Redis在裏面扮演了八爪魚多面手的角色:
Dashboard的海量通知的存儲。
Dashboard的二級索引。
存儲海量短連接的HBase前面的緩存。
Gearman Job Queue的存儲。
正在替換另外30臺memcached。
Instagram ,曾經,Redis powers their main feed, activity feed, sessions system, and other services。
但惋惜目前已遷往Cassandra,說新架構只需1/4的硬件費用,是的,就是那個致使Digg CTO辭職的Canssandra。
Flickr , 依然是asynchronous task system and rudimentary queueing system。以前Task system放在mysql innodb,根本,撐不住。
The Others:
Pinterest,混合使用MySQL、Membase與Redis做爲存儲。
Youporn.com,100%的Redis,MySQL只用於建立新需求用到的sorted set,300K QPS的大壓力。
日本微信 ,Redis在前負責異步Job Queue和O(n)的數據,且做爲O(n*t)數據的cache,HBase在後,負責O(n*t)數據, n是用戶,t是時間。
StackOverflow ,2 Redis servers for distribute caching,好窮好輕量。
Discourge,號稱是爲下一個十年打造的論壇系統, We use Redis for our job queue, rate limiting, as a cache and for transient data,恰好和我司的用法同樣。
情色網站 YouPorn,使用 Redis 進行數據存儲,Redis 服務器每秒處理30萬個頁面請求,每小時會記錄8-15GB數據。
extension modules項目封裝了經常使用的函數與場景,showcase example的src/demo/redis目錄裏有各場景的benchmark測試。
典型的Spring Template風格,和JdbcTemplate,HibernateTemplate同樣,封裝從JedisPool獲取與歸還Connecton的代碼,有帶返回值與無返回值兩種返回接口。
同時,對最經常使用的Jedis調用,直接封裝了一系列方法。
Scheduler實現了基於Redis的高併發單次定時任務分發。具體選型見Scheduler章節。
Master Elector基於redis setNx()與expire()兩個api實現,與基於Zookeeper,Hazelcast實現的效果相似。
計有Session,Counter,Scheduler 與 Master Elector四款。
daemonize no -> yes ,啓動daemonize模式,注意若是用daemon工具啓動redis-server時設回false。 logfile stdout -> /var/log/redis/redis.log ,指定日誌文件 註釋掉RDB的全部觸發規則,在Master不保存RDB文件。 dir ./ -> /var/data/redis,指定持久化文件及臨時文件目錄. maxmemory,設置爲可用內存/2. (可選)appendonly no->yes,打開AOF文件. auto-aof-rewrite-percentage 100, 綜合考慮硬盤大小,可接受重啓加載延時等儘可能的大,減小AOF rewrite頻率. auto-aof-rewrite-min-size 64mb,同上,起碼設爲5G. client-output-buffer-limit slave 256mb 64mb 60. 考慮Traffic及Slave同步是RDB加載所需時間,正確設置避免buffer撐爆client被關掉後又要從新進行全同步。
Master上的安全配置,可選。
設置RDB保存頻率,由於RDB只做爲Backup工具,只保留15分鐘的規則,設置爲15分鐘保存一次就夠了save 900 1。
(可選)slaveof 設置master地址,也可動態設定。
repl-timeout 60, 適當加大好比120,避免master實際還沒倒掉就認爲master倒了。
3.0.1版-3.0.3版 2013-8-1,在微博發佈後反應良好,持續修改。
3.0版 2013-6-29,在公司Workshop後修訂,提升wiki的可讀性而不僅是簡單的記錄知識點。
Redis的幾個認識誤區 by Tim yang。
轉載:@江南白衣