非常感謝http://blog.csdn.net/hguisu/article/details/9165141
網絡IO模型方面:Memcached是多線程,分為監聽線程、worker線程,引入鎖,帶來了性能損耗。Redis使用單線程的IO復用模型,將速度優勢發揮到最大,也提供了較簡單的計算功能
內存管理方面:Memcached使用預分配的內存池的方式,帶來一定程度的空間浪費 并且在內存仍然有很大空間時,新的數據也可能會被剔除,而Redis使用現場申請內存的方式來存儲數據,不會剔除任何非臨時數據 Redis更適合作為存儲而不是cache
數據的一致性方面:Memcached提供了cas命令來保證.而Redis提供了事務的功能,可以保證一串 命令的原子性,中間不會被任何操作打斷
1 、Redis不僅僅支持簡單的k/v類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
2 、Redis支持數據的備份,即master-slave模式的數據備份。
3 、Redis支持數據的持久化,可以將內存中的數據保持在磁盤中,重啟的時候可以再次加載進行使用。
4、Redis可以實現主從復制,實現故障恢復。
5、Redis的Sharding技術: 很容易將數據分布到多個Redis實例中
下面我們簡單一項一項詳解
http://redis.io/topics/data-types
2.1 Key
1、key越短越好:Redis是個內存數據庫,Key鍵越短你需要的空間就越少,因此key不能太長,比如1024字節。
舉個例子:在一個32位的Redis服務器上,如果儲存一百萬個鍵,每個值的長度是32-character,那么在使用6-character長度鍵名時,將會消耗大約96MB的空間,但是如果使用12-character長度的鍵名時,空間消耗則會提升至111MB左右。隨著鍵的增多,15%的額外開銷將產生重大的影響。
2、key命名要表達清楚意思。建議用”:”分隔域劃分鍵名,用”.”作為單詞間的連接,如”comment:1234:reply.to”。
使用合適的命名方法會簡化你的數據庫管理,當你通過你的應用程序或者服務做鍵的命名空間時,你就可以在數據遷移、轉換或者刪除時輕松的識別。
Redis另一個常見用例是作為熱數據項作的第二數據存儲,大部分的數據被保存在其他的數據庫中,比如PostgreSQL或MongoDB。在這些用例中,當數據從主存儲移除時,開發者經常會忘記刪除Redis中對應的數據。這種存在跨數據存儲的情況下,通常需要做級聯刪除,這種情況下,可以通過在Redis配置保存特定數據項的所有識別符來實現,從而保證數據在主數據庫被刪除后,系統會調用一個清理程序來刪除所有相關副本和信息。
2.2 String
String是最簡單的類型,一個key對應一個value,string類型是二進制安全的。Redis的string可以包含任何數據,比如jpg圖片或者序列化的對象(php中對象序列化函數serialize)
內部實現,其本質是一個byte數組,字符串的大小被限制在512M以內
[cpp] view plain copy PRint?所有常用命令的復雜度都是O(1),普通的Get/Set方法,可以用來做Cache,存session,為了簡化架構甚至可以替換掉Memcached。
Incr/IncrBy/IncrByFloat/Decr/DecrBy,可以用來做
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串一串。
如 果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的博客
原因分析:
官方文檔,由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。
完全重用已有功能,非常經濟先執行一次全同步 — 請求master BgSave出自己的一個RDB Snapshot文件發給slave,slave接收完畢后,清除掉自己的舊數據,然后將RDB載入內存。再進行增量同步 — master作為一個普通的client連入slave,將所有寫操作轉發給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)antirez 對 Sentinel的反駁,與下篇
另 外,sentinel會在master上建一個pub/sub channel,名為”sentinel:hello”,通告各種信息,sentinel們也是通過接收pub/sub channel上的+sentinel的信息發現彼此,因為每臺sentinel每5秒會發送一次自己的host信息,宣告自己的存在。
一是如果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。
用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過了,整個事務隊列都不會被執行。
官方文檔 與 《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,它占用的內存什么時候最終被清理掉只有天知道。
redis為了更好地實現這個功能,必須為不同的應用場景提供不同的策略,內存淘汰策略講的是為實現內存淘汰我們具體怎么做,要解決的問題包括淘汰鍵空間如何選擇?在鍵空間中淘汰鍵如何選擇?
Redis提供了下面幾種淘汰策略供用戶選擇,其中默認的策略為noeviction策略:
noeviction:當內存使用達到閾值的時候,所有引起申請內存的命令會報錯。
allkeys-lru:在主鍵空間中,優先移除最近未使用的key。
volatile-lru:在設置了過期時間的鍵空間中,優先移除最近未使用的key。
allkeys-random:在主鍵空間中,隨機移除某個key。
volatile-random:在設置了過期時間的鍵空間中,隨機移除某個key。
volatile-ttl:在設置了過期時間的鍵空間中,具有更早過期時間的key優先移除。
提示:主鍵空間和設置了過期時間的鍵空間,舉個例子,假設我們有一批鍵存儲在Redis中,則有那么一個哈希表用于存儲這批鍵及其值,如果這批鍵中有一部分設置了過期時間,那么這批鍵還會被存儲到另外一個哈希表中,這個哈希表中的值對應的是鍵被設置的過期時間。設置了過期時間的鍵空間為主鍵空間的子集。
我們了解了Redis大概提供了這么幾種淘汰策略,那么如何選擇呢?淘汰策略的選擇可以通過下面的配置指定:
# maxmemory-policy noeviction
但是這個值填什么呢?為解決這個問題,我們需要了解我們的應用請求對于Redis中存儲的數據集的訪問方式以及我們的訴求是什么。同時Redis也支持Runtime修改淘汰策略,這使得我們不需要重啟Redis實例而實時的調整內存淘汰策略。
下面看看幾種策略的適用場景:
allkeys-lru:如果我們的應用對緩存的訪問符合冪律分布(也就是存在相對熱點數據),或者我們不太清楚我們應用的緩存訪問分布狀況,我們可以選擇allkeys-lru策略。
allkeys-random:如果我們的應用對于緩存key的訪問概率相等,則可以使用這個策略。
volatile-ttl:這種策略使得我們可以向Redis提示哪些key更適合被eviction。
另外,volatile-lru策略和volatile-random策略適合我們將一個Redis實例既應用于緩存和又應用于持久化存儲的時候,然而我們也可以通過使用兩個Redis實例來達到相同的效果,值得一提的是將key設置過期時間實際上會消耗更多的內存,因此我們建議使用allkeys-lru策略從而更有效率的使用內存。
上面提到的LRU(Least Recently Used)策略,實際上Redis實現的LRU并不是可靠的LRU,也就是名義上我們使用LRU算法淘汰鍵,但是實際上被淘汰的鍵并不一定是真正的最久沒用的,這里涉及到一個權衡的問題,如果需要在全部鍵空間內搜索最優解,則必然會增加系統的開銷,Redis是單線程的,也就是同一個實例在每一個時刻只能服務于一個客戶端,所以耗時的操作一定要謹慎。為了在一定成本內實現相對的LRU,早期的Redis版本是基于采樣的LRU,也就是放棄全部鍵空間內搜索解改為采樣空間搜索最優解。自從Redis3.0版本之后,Redis作者對于基于采樣的LRU進行了一些優化,目的是在一定的成本內讓結果更靠近真實的LRU。
新聞熱點
疑難解答