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