在國家的正確指引和堅強領(lǐng)導下,在國內經(jīng)濟突飛猛進(jìn)一片光明的大好形勢下,隨著(zhù)互聯(lián)網(wǎng)的飛速發(fā)展,即使是普通互聯(lián)網(wǎng)應用的用戶(hù)數量也呈線(xiàn)性上升趨勢,更不用說(shuō)國外那些大型的廣受歡迎的諸多“并不存在”的網(wǎng)站們指數級的用戶(hù)增長(cháng)。而且網(wǎng)站內的數據關(guān)系也隨著(zhù)SNS等應用的興起發(fā)生了很大的變化。傳統的關(guān)系型數據庫已經(jīng)慢慢地對這些新時(shí)代的新特性顯得有些力不從心。如何提高單臺數據庫服務(wù)器負載能力,如何更高效更迅速地處理簡(jiǎn)單類(lèi)型的數據關(guān)系,成了擺在我們面前的亟待解決的問(wèn)題。
時(shí)隔多年又過(guò)了把寫(xiě)論文的癮。下面說(shuō)點(diǎn)有用的。
當初在設計這個(gè)項目的架構時(shí),就準備引入nosql作為主要組成部分。一是從網(wǎng)站的預期流量上,單臺mysql要撐起來(lái)還真有點(diǎn)費勁,mysql的擴展方案又不是很優(yōu)雅方便。二是當前凡是有點(diǎn)活力的場(chǎng)所,張口閉口都是nosql,現在搞個(gè)項目要是還在跟sql語(yǔ)句死摳較勁,你都不好意思跟人家打招呼。所以經(jīng)過(guò)一番論證和可行性分析,最終我們選擇了redis和mongodb來(lái)各負其責。這次先簡(jiǎn)單說(shuō)說(shuō)redis。
1、選擇理由 喜歡一個(gè)人是沒(méi)有理由的,但選擇一個(gè)組件,卻一定是要有理由的。這關(guān)系到日(名詞)后有沒(méi)有擴展空間,在項目中好不好用,大家寫(xiě)起代碼來(lái)會(huì )不會(huì )暗地里大罵當初那個(gè)選型的人。
redis是一款內存型的key-value數據庫,它允許把所有的數據都保留在內存里,保證了數據存取的速度。又有持久化和日志機制,保證了斷電時(shí)數據的完整性。redis支持hash、list、(sorted) set等數據類(lèi)型,作為絕大多數的應用來(lái)說(shuō)已經(jīng)足夠。而且redis的更新非???,開(kāi)發(fā)者們都很敬業(yè)努力,這也是選擇一個(gè)開(kāi)源組件的很重要的一個(gè)方面。
因為這個(gè)系列不是專(zhuān)門(mén)講解redis開(kāi)發(fā)的,所以更詳細的使用特性和開(kāi)發(fā)手冊,請微移蓮步至
官方網(wǎng)站。
2、適用場(chǎng)景 項目中使用redis的場(chǎng)景主要有以下幾處:
2.1 rails默認緩存。凡是rails需要使用緩存的地方,比如頁(yè)面片段緩存等,都會(huì )用到指定的默認緩存系統。這個(gè)配置起來(lái)很簡(jiǎn)單,只需要一行代碼即可,而且也不必關(guān)心rails具體在redis上是怎么實(shí)現的,自有
redis_store來(lái)完成這一切。
- config.cache_store = :redis_store, $config.redis[:server]
config.cache_store = :redis_store, $config.redis[:server]
2.2 自定義緩存。主要是以對象緩存的形式,保存在開(kāi)發(fā)中認為有必要進(jìn)行快速存取的數據。自定義緩存需要自己寫(xiě)一個(gè)類(lèi),通過(guò)redis store調用
redis client的命令,來(lái)實(shí)現數據的存取。比如首頁(yè)上需要調用的某些資訊數據,就不再每次都從mysql中獲取,而是由后臺任務(wù)定時(shí)從mysql中讀取或在內容更新時(shí)讀取并保存至redis緩存中。
其中要注意一點(diǎn),redis保存的value值,只接受字符串格式,所以如果要通過(guò)自定義緩存保存非字符串型的數據,就需要使用Marshal進(jìn)行序列化和反序列化。
2.3 任務(wù)隊列。執行異步和定時(shí)任務(wù)的resque和resque-scheduler組件,使用redis作為任務(wù)隊列服務(wù)器。同樣,按照resque的配置說(shuō)明,一行代碼即可搞定。
- Resque.redis = Redis.new($config.redis[:server])
Resque.redis = Redis.new($config.redis[:server])
3、擴展redis緩存 redis_store只是按照ActiveSupport::Cache的規范實(shí)現了諸如read、write、increment、decrement、delete等通用的存取接口,而作為redis一大亮點(diǎn)的hash、set等數據結構則在默認的規范中沒(méi)有用武之地。而且在項目中,很有可能會(huì )有存取hash類(lèi)型緩存的需求。
作為金融資訊網(wǎng)站,當天的股票行情信息是非常重要的,訪(fǎng)問(wèn)率非常高,而且要求訪(fǎng)問(wèn)速度很快,如果每次訪(fǎng)問(wèn)都要去oracle實(shí)時(shí)查詢(xún),則無(wú)法滿(mǎn)足速度的要求。因此,當天所有的股票行情數據,我們從oracle中取出之后,都要保存redis的高速緩存中。
國內的股票一共有2000多支,每支股票的行情數據要按照不低于每分鐘一次的頻率進(jìn)行實(shí)時(shí)刷新。如果每支股票的數據都存為一個(gè)key-value鍵值對,那么在進(jìn)行每分鐘更新時(shí),要同時(shí)取出2000個(gè)鍵值對,反序列化,對每支股票依次插入最新的行情數據,再依次序列化保存。經(jīng)過(guò)實(shí)際測試,循環(huán)2000次序列化和反序列化所用時(shí)間極長(cháng),想在1分鐘內完成這個(gè)任務(wù)是不可能的。
因此這就是一個(gè)典型的hash類(lèi)型緩存存取的需求。我們把這2000支股票數據作為一個(gè)hash來(lái)進(jìn)行保存,key是:stocks,field就是每支股票的代碼,這樣就不需要循環(huán)2000次存取數據,而只需一個(gè)redis命令就能完成所有2000多支股票數據的保存和讀取,滿(mǎn)足了在一分鐘內實(shí)時(shí)刷新行情數據的要求。而且如果要讀取某一支股票的數據,也只需指定key和field,就可迅速取出數據。
實(shí)現方法是擴展redis_store的RedisStore::Cache::Store類(lèi)。具體代碼就很簡(jiǎn)單了,這也顯示出了redis的功能強大和ruby編程的便利。
- def hwrite(key, hash)
- @data.hmset(key, *hash.map{|k, v| [k, Marshal.dump(v)]}.flatten(1))
- end
-
- def hread(key, field = nil)
- field.nil? ? Hash[*@data.hgetall(key).map{|k, v| [k, Marshal.load(v)]}.flatten(1)] :
- Marshal.load(@data.hget(key, field))
- rescue TypeError
- end
def hwrite(key, hash) @data.hmset(key, *hash.map{|k, v| [k, Marshal.dump(v)]}.flatten(1))enddef hread(key, field = nil) field.nil? ? Hash[*@data.hgetall(key).map{|k, v| [k, Marshal.load(v)]}.flatten(1)] : Marshal.load(@data.hget(key, field))rescue TypeErrorend其中@data是Redis::Factory創(chuàng )建的一個(gè)Redis::Store實(shí)例,負責調用redis client執行redis命令。
同樣,如果在項目中需要list和set等數據類(lèi)型的緩存,也可按此思路一并處理。
4、redis高可用 因為redis不僅作為緩存使用,而且也是resque執行異步和定時(shí)任務(wù)的消息隊列,因此對于可用性的要求就比較高,一旦掛掉,所有后臺任務(wù)就會(huì )全部停止,嚴重影響網(wǎng)站的功能和體驗。
但是redis原生的cluster解決方案遲遲不出,去年看redis官網(wǎng)的時(shí)候,說(shuō)是直到今年5月份才可能會(huì )有rc放出,所以沒(méi)辦法,只能自己做一個(gè)山寨的高可用方案勉強支撐一段時(shí)間。
PS:今年5月份的時(shí)候我再看,卻又拖到“不早于夏末”了。原來(lái)不只是XXX說(shuō)話(huà)不算數的。
redis雙機高可用的基礎,是redis的主備復制機制。指定主備角色,是用slaveof命令。
指定本機為master
slaveof NO ONE
指定本機為192.168.1.10的slave
- slaveof 192.168.1.10 6379
slaveof 192.168.1.10 6379
本來(lái)一開(kāi)始我也想如同mysql的master-master機制那樣,分別在配置文件中指定本機為對方的slave,不過(guò)后來(lái)發(fā)現這個(gè)方法行不通。如果配置文件中都設置slaveof x.x.x.x,那么這兩個(gè)redis啟動(dòng)之后不提供服務(wù),客戶(hù)端無(wú)法連接,類(lèi)似于服務(wù)死鎖的狀態(tài)。
經(jīng)過(guò)多次實(shí)驗發(fā)現,如果兩個(gè)服務(wù)按照master-slave的方式啟動(dòng),然后給master發(fā)送slaveof命令,指定其為另一個(gè)的slave,則此時(shí)雙方都為slave,數據可以進(jìn)行雙向同步?;谶@個(gè)原理,設計了一個(gè)redis雙機互備的機制。
在自定義的配置文件中,做如下配置:
- redis:
- server: redis://192.168.1.1:6379
- cluster:
- master: redis://192.168.1.10:6379
- slave: redis://192.168.1.20:6379
redis: server: redis://192.168.1.1:6379 cluster: master: redis://192.168.1.10:6379 slave: redis://192.168.1.20:6379
192.168.1.1是keepalived的virtual ip,應用程序只使用這個(gè)ip地址來(lái)存取redis。
其核心的實(shí)現方式如下:
4.1 兩臺redis服務(wù)器,配合keepalived。初始狀態(tài),是在master(192.168.1.10)上綁定keepalived的virtual ip 192.168.1.1。
4.2 啟動(dòng)一個(gè)監控腳本,每秒鐘對兩個(gè)redis服務(wù)進(jìn)行一次掃描。
4.3 如果兩臺redis處于正常master-slave狀態(tài),則不進(jìn)行操作。
4.4 如果master掛掉,監控腳本對在線(xiàn)的slave(192.168.1.20)發(fā)送slaveof NO ONE命令,設置其為臨時(shí)的主機temp-master,同時(shí)由于原來(lái)的master服務(wù)器掛掉,virtual ip 192.168.1.1自動(dòng)轉移至temp-master,不影響應用程序對redis的存取。此時(shí)應用程序新產(chǎn)生的數據都保存到temp-master(192.168.1.20)上。
4.5 腳本監測到原來(lái)的master(192.168.1.10)在掛掉后重新啟動(dòng)加入集群,則向master發(fā)送slaveof 192.168.1.20 6379命令,設置其為temp-slave,從temp-master(192.168.1.20)復制在自己掛掉期間丟失的數據。同時(shí)virtual ip自動(dòng)跳回temp-slave(192.168.1.10)向應用程序提供服務(wù)。
4.6 延時(shí)30秒鐘,確保數據復制完畢,對調temp-master和temp-slave的角色,恢復默認的master-slave體系。
我知道延時(shí)30秒鐘確保數據復制完畢這種方式很不好,但我確實(shí)在redis的info命令響應中沒(méi)有找到指示復制完畢的字段。如果有消息能夠明確指出數據復制完畢的狀態(tài)會(huì )更好。
這樣,兩臺redis服務(wù)器中的任何一臺掛掉,都會(huì )由另一臺繼續提供服務(wù),不會(huì )對網(wǎng)站形成可察覺(jué)的影響,也不會(huì )丟失數據。
5、redis配置 redis的配置也比較靈活強大,使得redis的使用也方便了不少。
5.1 持久化頻率。配置save a b,指定在a秒內如果有b次key的改變,就執行硬盤(pán)持久化。此頻率根據服務(wù)器狀態(tài)進(jìn)行設定,最好不要太過(guò)頻繁。
5.2 內存限制。使用maxmemory,限制最大使用內存,如數據超出這個(gè)大小,則按照LRU把最不常用的移出redis。這個(gè)特性對于使用內存有限的VPS時(shí)比較適合,免得內存超出之后造成宕機或天量收費。
5.3 虛擬內存。設置vm-enabled,可指定redis能夠使用的最大物理內存,當存儲數據大于此內存值時(shí),按照LRU算法把最不常使用的value移出到硬盤(pán)的虛擬內存文件中。不過(guò)所有的key都是保存在內存中的,這個(gè)不可設置。
5.4 二進(jìn)制日志。當然,redis可以設置5.1所述的save參數,但如果存盤(pán)動(dòng)作太密集,則會(huì )占用很多的資源,速度一慢也就失去了內存數據庫的主要優(yōu)點(diǎn)。為此redis設計了日志機制。通過(guò)設置appendonly,可以開(kāi)啟日志選項,每一個(gè)發(fā)送到redis執行的命令,都會(huì )被立刻追加到硬盤(pán)的日志文件中,如果redis意外宕機,則在重新啟動(dòng)的時(shí)候,redis會(huì )讀取日志里的內容,恢復內存中尚未持久化的數據。
不過(guò)因為appendonly是所有數據的累積,所以文件大小增長(cháng)非???,在我們的項目中,差不多每一個(gè)小時(shí)就會(huì )增長(cháng)6個(gè)G。雖然appendonly是另開(kāi)進(jìn)程操作的,但文件太大也會(huì )影響效率,更何況還有塞滿(mǎn)硬盤(pán)的危險。為此我們使用定時(shí)任務(wù),每半個(gè)小時(shí)向redis發(fā)送bgrewriteaof命令,使redis按照當前數據快照重寫(xiě)日志,重寫(xiě)后的日志大小與內存數據大小在同一個(gè)數量級上