Web應(yīng)用的緩存設(shè)計(jì)模式舉例分析

本篇內(nèi)容介紹了“Web應(yīng)用的緩存設(shè)計(jì)模式舉例分析”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

站在用戶(hù)的角度思考問(wèn)題,與客戶(hù)深入溝通,找到杜集網(wǎng)站設(shè)計(jì)與杜集網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個(gè)性化、用戶(hù)體驗(yàn)好的作品,建站類(lèi)型包括:網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、國(guó)際域名空間、網(wǎng)絡(luò)空間、企業(yè)郵箱。業(yè)務(wù)覆蓋杜集地區(qū)。

ORM緩存引言

從10年前的2003年開(kāi)始,在Web應(yīng)用領(lǐng)域,ORM(對(duì)象-關(guān)系映射)框架就開(kāi)始逐漸普及,并且流行開(kāi)來(lái),其中最廣為人知的就是Java的開(kāi)源ORM框架Hibernate,后來(lái)Hibernate也成為了EJB3的實(shí)現(xiàn)框架;2005年以后,ORM開(kāi)始普及到其他編程語(yǔ)言領(lǐng)域,其中最有名氣的是Ruby on rails框架的ORM - ActiveRecord。如今各種開(kāi)源框架的ORM,乃至ODM(對(duì)象-文檔關(guān)系映射,用在訪問(wèn)NOSQLDB)層出不窮,功能都十分強(qiáng)大,也很普及。

然而圍繞ORM的性能問(wèn)題,也一直有很多批評(píng)的聲音。其實(shí)ORM的架構(gòu)對(duì)插入緩存技術(shù)是非常容易的,我做的很多項(xiàng)目和產(chǎn)品,但凡使用ORM,緩存都是標(biāo)配,性能都非常好。而且我發(fā)現(xiàn)業(yè)界使用ORM的案例都忽視了緩存的運(yùn)用,或者說(shuō)沒(méi)有意識(shí)到ORM緩存可以帶來(lái)巨大的性能提升。

ORM緩存應(yīng)用案例

我們?nèi)ツ暧幸粋€(gè)老產(chǎn)品重寫(xiě)的項(xiàng)目,這個(gè)產(chǎn)品有超過(guò)10年歷史了,數(shù)據(jù)庫(kù)的數(shù)據(jù)量很大,多個(gè)表都是上千萬(wàn)條記錄,最大的表記錄達(dá)到了9000萬(wàn)條,Web訪問(wèn)的請(qǐng)求數(shù)每天有300萬(wàn)左右。

老產(chǎn)品采用了傳統(tǒng)的解決性能問(wèn)題的方案:Web層采用了動(dòng)態(tài)頁(yè)面靜態(tài)化技術(shù),超過(guò)一定時(shí)間的文章生成靜態(tài)HTML文件;對(duì)數(shù)據(jù)庫(kù)進(jìn)行分庫(kù)分表,按年拆表。動(dòng)態(tài)頁(yè)面靜態(tài)化和分庫(kù)分表是應(yīng)對(duì)大訪問(wèn)量和大數(shù)據(jù)量的常規(guī)手段,本身也有效。但它的缺點(diǎn)也很多,比方說(shuō)增加了代碼復(fù)雜度和維護(hù)難度,跨庫(kù)運(yùn)算的困難等等,這個(gè)產(chǎn)品的代碼維護(hù)歷來(lái)非常困難,導(dǎo)致bug很多。

進(jìn)行產(chǎn)品重寫(xiě)的時(shí)候,我們放棄了動(dòng)態(tài)頁(yè)面靜態(tài)化,采用了純動(dòng)態(tài)網(wǎng)頁(yè);放棄了分庫(kù)分表,直接操作千萬(wàn)級(jí),乃至近億條記錄的大表進(jìn)行SQL查詢(xún);也沒(méi)有采取讀寫(xiě)分離技術(shù),全部查詢(xún)都是在單臺(tái)主數(shù)據(jù)庫(kù)上進(jìn)行;數(shù)據(jù)庫(kù)訪問(wèn)全部使用ActiveRecord,進(jìn)行了大量的ORM緩存。上線以后的效果非常好:?jiǎn)闻_(tái)MySQL數(shù)據(jù)庫(kù)服務(wù)器CPU的IO Wait低于5%;用單臺(tái)1U服務(wù)器2顆4核至強(qiáng)CPU已經(jīng)可以輕松支持每天350萬(wàn)動(dòng)態(tài)請(qǐng)求量;最重要的是,插入緩存并不需要代碼增加多少?gòu)?fù)雜度,可維護(hù)性非常好。

總之,采用ORM緩存是Web應(yīng)用提升性能一種有效的思路,這種思路和傳統(tǒng)的提升性能的解決方案有很大的不同,但它在很多應(yīng)用場(chǎng)景(包括高度動(dòng)態(tài)化的SNS類(lèi)型應(yīng)用)非常有效,而且不會(huì)顯著增加代碼復(fù)雜度,所以這也是我自己一直偏愛(ài)的方式。因此我一直很想寫(xiě)篇文章,結(jié)合示例代碼介紹ORM緩存的編程技巧。

今年春節(jié)前后,我開(kāi)發(fā)自己的個(gè)人網(wǎng)站項(xiàng)目,有意識(shí)的大量使用了ORM緩存技巧。對(duì)一個(gè)沒(méi)多少訪問(wèn)量的個(gè)人站點(diǎn)來(lái)說(shuō),有些過(guò)度設(shè)計(jì)了,但我也想借這個(gè)機(jī)會(huì)把常用的ORM緩存設(shè)計(jì)模式寫(xiě)成示例代碼,提供給大家參考。我的個(gè)人網(wǎng)站源代碼是開(kāi)源的,托管在github上:robbin_site

ORM緩存的基本理念

·我在2007年的時(shí)候?qū)戇^(guò)一篇文章,分析ORM緩存的理念:ORM對(duì)象緩存探討 ,所以這篇文章不展開(kāi)詳談了,總結(jié)來(lái)說(shuō),ORM緩存的基本理念是:

·以減少數(shù)據(jù)庫(kù)服務(wù)器磁盤(pán)IO為最終目的,而不是減少發(fā)送到數(shù)據(jù)庫(kù)的SQL條數(shù)。實(shí)際上使用ORM,會(huì)顯著增加SQL條數(shù),有時(shí)候會(huì)成倍增加SQL。

·數(shù)據(jù)庫(kù)schema設(shè)計(jì)的取向是盡量設(shè)計(jì) 細(xì)顆粒度 的表,表和表之間用外鍵關(guān)聯(lián),顆粒度越細(xì),緩存對(duì)象的單位越小,緩存的應(yīng)用場(chǎng)景越廣泛

盡量避免多表關(guān)聯(lián)查詢(xún),盡量拆成多個(gè)表單獨(dú)的主鍵查詢(xún),盡量多制造 n + 1 條查詢(xún),不要害怕“臭名昭著”的 n + 1 問(wèn)題,實(shí)際上 n + 1 才能有效利用ORM緩存

利用表關(guān)聯(lián)實(shí)現(xiàn)透明的對(duì)象緩存

在設(shè)計(jì)數(shù)據(jù)庫(kù)的schema的時(shí)候,設(shè)計(jì)多個(gè)細(xì)顆粒度的表,用外鍵關(guān)聯(lián)起來(lái)。當(dāng)通過(guò)ORM訪問(wèn)關(guān)聯(lián)對(duì)象的時(shí)候,ORM框架會(huì)將關(guān)聯(lián)對(duì)象的訪問(wèn)轉(zhuǎn)化成用主鍵查詢(xún)關(guān)聯(lián)表,發(fā)送 n + 1條SQL。而基于主鍵的查詢(xún)可以直接利用對(duì)象緩存。

我們自己開(kāi)發(fā)了一個(gè)基于ActiveRecord封裝的對(duì)象緩存框架:second_level_cache ,從這個(gè)ruby插件的名稱(chēng)就可以看出,實(shí)現(xiàn)借鑒了Hibernate的二級(jí)緩存實(shí)現(xiàn)。這個(gè)對(duì)象緩存的配置和使用,可以看我寫(xiě)的ActiveRecord對(duì)象緩存配置 。

下面用一個(gè)實(shí)際例子來(lái)演示一下對(duì)象緩存起到的作用:訪問(wèn)我個(gè)人站點(diǎn)的首頁(yè)。 這個(gè)頁(yè)面的數(shù)據(jù)需要讀取三張表:blogs表獲取文章信息,blog_contents表獲取文章內(nèi)容,accounts表獲取作者信息。三張表的model定義片段如下,完整代碼請(qǐng)看models :

class Account < ActiveRecord::Base    acts_as_cached    has_many :blogs  end  class Blog < ActiveRecord::Base    acts_as_cached    belongs_to :blog_content, :dependent => :destroy     belongs_to :account, :counter_cache => true end  class BlogContent < ActiveRecord::Base    acts_as_cached  end

傳統(tǒng)的做法是發(fā)送一條三表關(guān)聯(lián)的查詢(xún)語(yǔ)句,類(lèi)似這樣的:

SELECT blogs.*, blog_contents.content, account.name       FROM blogs       LEFT JOIN blog_contents ON blogs.blog_content_id = blog_contents.id       LEFT JOIN accounts ON blogs.account_id = account.id

往往單條SQL語(yǔ)句就搞定了,但是復(fù)雜SQL的帶來(lái)的表掃描范圍可能比較大,造成的數(shù)據(jù)庫(kù)服務(wù)器磁盤(pán)IO會(huì)高很多,數(shù)據(jù)庫(kù)實(shí)際IO負(fù)載往往無(wú)法得到有效緩解。

我的做法如下,完整代碼請(qǐng)看home.rb :

@blogs = Blog.order('id DESC').page(params[:page])

這是一條分頁(yè)查詢(xún),實(shí)際發(fā)送的SQL如下:

SELECT * FROM blogs ORDER BY id DESC LIMIT 20

轉(zhuǎn)成了單表查詢(xún),磁盤(pán)IO會(huì)小很多。至于文章內(nèi)容,則是通過(guò)blog.content的對(duì)象訪問(wèn)獲得的,由于首頁(yè)抓取20篇文章,所以實(shí)際上會(huì)多出來(lái)20條主鍵查詢(xún)SQL訪問(wèn)blog_contents表。就像下面這樣:

DEBUG -  BlogContent Load (0.3ms)  SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 29 LIMIT 1  DEBUG -  BlogContent Load (0.2ms)  SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 28 LIMIT 1  DEBUG -  BlogContent Load (1.3ms)  SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 27 LIMIT 1  ......  DEBUG -  BlogContent Load (0.9ms)  SELECT `blog_contents`.* FROM `blog_contents` WHERE `blog_contents`.`id` = 10 LIMIT 1

但是主鍵查詢(xún)SQL不會(huì)造成表的掃描,而且往往已經(jīng)被數(shù)據(jù)庫(kù)buffer緩存,所以基本不會(huì)發(fā)生數(shù)據(jù)庫(kù)服務(wù)器的磁盤(pán)IO,因而總體的數(shù)據(jù)庫(kù)IO負(fù)載會(huì)遠(yuǎn)遠(yuǎn)小于前者的多表聯(lián)合查詢(xún)。特別是當(dāng)使用對(duì)象緩存之后,會(huì)緩存所有主鍵查詢(xún)語(yǔ)句,這20條SQL語(yǔ)句往往并不會(huì)全部發(fā)生,特別是熱點(diǎn)數(shù)據(jù),緩存命中率很高:

DEBUG -  Cache read: robbin/blog/29/1  DEBUG -  Cache read: robbin/account/1/0  DEBUG -  Cache read: robbin/blogcontent/29/0  DEBUG -  Cache read: robbin/account/1/0  DEBUG -  Cache read: robbin/blog/28/1  ......  DEBUG -  Cache read: robbin/blogcontent/11/0  DEBUG -  Cache read: robbin/account/1/0  DEBUG -  Cache read: robbin/blog/10/1  DEBUG -  Cache read: robbin/blogcontent/10/0  DEBUG -  Cache read: robbin/account/1/0

拆分n+1條查詢(xún)的方式,看起來(lái)似乎非常違反大家的直覺(jué),但實(shí)際上這是真理,我實(shí)踐經(jīng)驗(yàn)證明:數(shù)據(jù)庫(kù)服務(wù)器的瓶頸往往是磁盤(pán)IO,而不是SQL并發(fā)數(shù)量。因此 拆分n+1條查詢(xún)本質(zhì)上是以增加n條SQL語(yǔ)句為代價(jià),簡(jiǎn)化復(fù)雜SQL,換取數(shù)據(jù)庫(kù)服務(wù)器磁盤(pán)IO的降低當(dāng)然這樣做以后,對(duì)于ORM來(lái)說(shuō),有額外的好處,就是可以高效的使用緩存了。

按照column拆表實(shí)現(xiàn)細(xì)粒度對(duì)象緩存

數(shù)據(jù)庫(kù)的瓶頸往往在磁盤(pán)IO上,所以應(yīng)該盡量避免對(duì)大表的掃描。傳統(tǒng)的拆表是按照row去拆分,保持表的體積不會(huì)過(guò)大,但是缺點(diǎn)是造成應(yīng)用代碼復(fù)雜度很高;使用ORM緩存的辦法,則是按照column進(jìn)行拆表,原則一般是:

&middot;將大字段拆分出來(lái),放在一個(gè)單獨(dú)的表里面,表只有主鍵和大字段,外鍵放在主表當(dāng)中

&middot;將不參與where條件和統(tǒng)計(jì)查詢(xún)的字段拆分出來(lái),放在獨(dú)立的表中,外鍵放在主表當(dāng)中

按照column拆表本質(zhì)上是一個(gè)去關(guān)系化的過(guò)程。主表只保留參與關(guān)系運(yùn)算的字段,將非關(guān)系型的字段剝離到關(guān)聯(lián)表當(dāng)中,關(guān)聯(lián)表僅允許主鍵查詢(xún),以Key-Value DB的方式來(lái)訪問(wèn)。因此這種緩存設(shè)計(jì)模式本質(zhì)上是一種SQLDB和NoSQLDB的混合架構(gòu)設(shè)計(jì)

下面看一個(gè)實(shí)際的例子:文章的內(nèi)容content字段是一個(gè)大字段,該字段不能放在blogs表中,否則會(huì)造成blogs表過(guò)大,表掃描造成較多的磁盤(pán)IO。我實(shí)際做法是創(chuàng)建blog_contents表,保存content字段,schema簡(jiǎn)化定義如下:

CREATE TABLE `blogs` (    `id` int(11) NOT NULL AUTO_INCREMENT,    `title` varchar(255) NOT NULL,    `blog_content_id` int(11) NOT NULL,    `content_updated_at` datetime DEFAULT NULL,    PRIMARY KEY (`id`),  );   CREATE TABLE `blog_contents` (    `id` int(11) NOT NULL AUTO_INCREMENT,    `content` mediumtext NOT NULL,    PRIMARY KEY (`id`)  );

blog_contents表只有content大字段,其外鍵保存到主表blogs的blog_content_id字段里面。

model定義和相關(guān)的封裝如下:

class Blog < ActiveRecord::Base    acts_as_cached    delegate :content, :to => :blog_content, :allow_nil => true    def content=(value)      self.blog_content ||= BlogContent.new      self.blog_content.content = value      self.content_updated_at = Time.now    end end  class BlogContent < ActiveRecord::Base    acts_as_cached    validates :content, :presence => true end

在Blog類(lèi)上定義了虛擬屬性content,當(dāng)訪問(wèn)blog.content的時(shí)候,實(shí)際上會(huì)發(fā)生一條主鍵查詢(xún)的SQL語(yǔ)句,獲取blog_content.content內(nèi)容。由于BlogContent上面定義了對(duì)象緩存acts_as_cached,只要被訪問(wèn)過(guò)一次,content內(nèi)容就會(huì)被緩存到memcached里面。

這種緩存技術(shù)實(shí)際會(huì)非常有效,因?yàn)椋?只要緩存足夠大,所有文章內(nèi)容可以全部被加載到緩存當(dāng)中,無(wú)論文章內(nèi)容表有多么大,你都不需要再訪問(wèn)數(shù)據(jù)庫(kù)了 更進(jìn)一步的是: 這張大表你永遠(yuǎn)都只需要通過(guò)主鍵進(jìn)行訪問(wèn),絕無(wú)可能出現(xiàn)表掃描的狀況 為何當(dāng)數(shù)據(jù)量大到9000萬(wàn)條記錄以后,我們的系統(tǒng)仍然能夠保持良好的性能,秘密就在于此。

還有一點(diǎn)非常重要: 使用以上兩種對(duì)象緩存的設(shè)計(jì)模式,你除了需要添加一條緩存聲明語(yǔ)句acts_as_cached以外,不需要顯式編寫(xiě)一行代碼 有效利用緩存的代價(jià)如此之低,何樂(lè)而不為呢?

以上兩種緩存設(shè)計(jì)模式都不需要顯式編寫(xiě)緩存代碼,以下的緩存設(shè)計(jì)模式則需要編寫(xiě)少量的緩存代碼,不過(guò)代碼的增加量非常少。

寫(xiě)一致性緩存

寫(xiě)一致性緩存,叫做write-through cache,是一個(gè)CPU Cache借鑒過(guò)來(lái)的概念,意思是說(shuō),當(dāng)數(shù)據(jù)庫(kù)記錄被修改以后,同時(shí)更新緩存,不必進(jìn)行額外的緩存過(guò)期處理操作。但在應(yīng)用系統(tǒng)中,我們需要一點(diǎn)技巧來(lái)實(shí)現(xiàn)寫(xiě)一致性緩存。來(lái)看一個(gè)例子:

我的網(wǎng)站文章原文是markdown格式的,當(dāng)頁(yè)面顯示的時(shí)候,需要轉(zhuǎn)換成html的頁(yè)面,這個(gè)轉(zhuǎn)換過(guò)程本身是非常消耗CPU的,我使用的是Github的markdown的庫(kù)。Github為了提高性能,用C寫(xiě)了轉(zhuǎn)換庫(kù),但如果是非常大的文章,仍然是一個(gè)耗時(shí)的過(guò)程,Ruby應(yīng)用服務(wù)器的負(fù)載就會(huì)比較高。

我的解決辦法是緩存markdown原文轉(zhuǎn)換好的html頁(yè)面的內(nèi)容,這樣當(dāng)再次訪問(wèn)該頁(yè)面的時(shí)候,就不必再次轉(zhuǎn)換了,直接從緩存當(dāng)中取出已經(jīng)緩存好的頁(yè)面內(nèi)容即可,極大提升了系統(tǒng)性能。我的網(wǎng)站文章最終頁(yè)的代碼執(zhí)行時(shí)間開(kāi)銷(xiāo)往往小于10ms,就是這個(gè)原因。代碼如下:

def md_content  # cached markdown format blog content    APP_CACHE.fetch(content_cache_key) { GitHub::Markdown.to_html(content, :gfm) }  end

這里存在一個(gè)如何進(jìn)行緩存過(guò)期的問(wèn)題,當(dāng)文章內(nèi)容被修改以后,應(yīng)該更新緩存內(nèi)容,讓老的緩存過(guò)期,否則就會(huì)出現(xiàn)數(shù)據(jù)不一致的現(xiàn)象。進(jìn)行緩存過(guò)期處理是比較麻煩的,我們可以利用一個(gè)技巧來(lái)實(shí)現(xiàn)自動(dòng)緩存過(guò)期:

def content_cache_key    "#{CACHE_PREFIX}/blog_content/#{self.id}/#{content_updated_at.to_i}" end

當(dāng)構(gòu)造緩存對(duì)象的key的時(shí)候,我用文章內(nèi)容被更新的時(shí)間來(lái)構(gòu)造key值,這個(gè)文章內(nèi)容更新時(shí)間用的是blogs表的content_updated_at字段,當(dāng)文章被更新的時(shí)候,blogs表會(huì)進(jìn)行update,更新該字段。因此每當(dāng)文章內(nèi)容被更新,緩存的頁(yè)面內(nèi)容的key就會(huì)改變,應(yīng)用程序下次訪問(wèn)文章頁(yè)面的時(shí)候,緩存就會(huì)失效,于是重新調(diào)用GitHub::Markdown.to_html(content, :gfm)生成新的頁(yè)面內(nèi)容。 而老的頁(yè)面緩存內(nèi)容再也不會(huì)被應(yīng)用程序存取,根據(jù)memcached的LRU算法,當(dāng)緩存填滿之后,將被優(yōu)先剔除。

除了文章內(nèi)容緩存之外,文章的評(píng)論內(nèi)容轉(zhuǎn)換成html以后也使用了這種緩存設(shè)計(jì)模式。具體可以看相應(yīng)的源代碼:blog_comment.rb

片段緩存和過(guò)期處理

Web應(yīng)用當(dāng)中有大量的并非實(shí)時(shí)更新的數(shù)據(jù),這些數(shù)據(jù)都可以使用緩存,避免每次存取的時(shí)候都進(jìn)行數(shù)據(jù)庫(kù)查詢(xún)和運(yùn)算。這種片段緩存的應(yīng)用場(chǎng)景很多,例如:

&middot;展示網(wǎng)站的Tag分類(lèi)統(tǒng)計(jì)(只要沒(méi)有更新文章分類(lèi),或者發(fā)布新文章,緩存一直有效)

&middot;輸出網(wǎng)站RSS(只要沒(méi)有發(fā)新文章,緩存一直有效)

&middot;網(wǎng)站右側(cè)欄(如果沒(méi)有新的評(píng)論或者發(fā)布新文章,則在一段時(shí)間例如一天內(nèi)基本不需要更新)

以上應(yīng)用場(chǎng)景都可以使用緩存,代碼示例:

def self.cached_tag_cloud    APP_CACHE.fetch("#{CACHE_PREFIX}/blog_tags/tag_cloud") do      self.tag_counts.sort_by(&:count).reverse    end end

對(duì)全站文章的Tag云進(jìn)行查詢(xún),對(duì)查詢(xún)結(jié)果進(jìn)行緩存

<% cache("#{CACHE_PREFIX}/layout/right", :expires_in => 1.day) do %>  <div class="tag">   <% Blog.cached_tag_cloud.select {|t| t.count > 2}.each do |tag| %>   <%= link_to "#{tag.name}<span>#{tag.count}</span>".html_safe, url(:blog, :tag, :name => tag.name) %>   <% end %> </div> ......  <% end %>

對(duì)全站右側(cè)欄頁(yè)面進(jìn)行緩存,過(guò)期時(shí)間是1天。

緩存的過(guò)期處理往往是比較麻煩的事情,但在ORM框架當(dāng)中,我們可以利用model對(duì)象的回調(diào),很容易實(shí)現(xiàn)緩存過(guò)期處理。我們的緩存都是和文章,以及評(píng)論相關(guān)的,所以可以直接注冊(cè)Blog類(lèi)和BlogComment類(lèi)的回調(diào)接口,聲明當(dāng)對(duì)象被保存或者刪除的時(shí)候調(diào)用刪除方法:

class Blog < ActiveRecord::Base   acts_as_cached    after_save :clean_cache    before_destroy :clean_cache    def clean_cache      APP_CACHE.delete("#{CACHE_PREFIX}/blog_tags/tag_cloud")   # clean tag_cloud      APP_CACHE.delete("#{CACHE_PREFIX}/rss/all")               # clean rss cache      APP_CACHE.delete("#{CACHE_PREFIX}/layout/right")          # clean layout right column cache in _right.erb    end  end   class BlogComment < ActiveRecord::Base   acts_as_cached    after_save :clean_cache    before_destroy :clean_cache    def clean_cache      APP_CACHE.delete("#{CACHE_PREFIX}/layout/right")     # clean layout right column cache in _right.erb    end  end

在Blog對(duì)象的after_save和before_destroy上注冊(cè)clean_cache方法,當(dāng)文章被修改或者刪除的時(shí)候,刪除以上緩存內(nèi)容。總之,可以利用ORM對(duì)象的回調(diào)接口進(jìn)行緩存過(guò)期處理,而不需要到處寫(xiě)緩存清理代碼。

對(duì)象寫(xiě)入緩存

我們通常說(shuō)到緩存,總是認(rèn)為緩存是提升應(yīng)用讀取性能的,其實(shí)緩存也可以有效的提升應(yīng)用的寫(xiě)入性能。我們看一個(gè)常見(jiàn)的應(yīng)用場(chǎng)景:記錄文章點(diǎn)擊次數(shù)這個(gè)功能。

文章點(diǎn)擊次數(shù)需要每次訪問(wèn)文章頁(yè)面的時(shí)候,都要更新文章的點(diǎn)擊次數(shù)字段view_count,然后文章必須實(shí)時(shí)顯示文章的點(diǎn)擊次數(shù),因此常見(jiàn)的讀緩存模式完全無(wú)效了。每次訪問(wèn)都必須更新數(shù)據(jù)庫(kù),當(dāng)訪問(wèn)量很大以后數(shù)據(jù)庫(kù)是吃不消的,因此我們必須同時(shí)做到兩點(diǎn):

&middot;每次文章頁(yè)面被訪問(wèn),都要實(shí)時(shí)更新文章的點(diǎn)擊次數(shù),并且顯示出來(lái)

&middot;不能每次文章頁(yè)面被訪問(wèn),都更新數(shù)據(jù)庫(kù),否則數(shù)據(jù)庫(kù)吃不消

對(duì)付這種應(yīng)用場(chǎng)景,我們可以利用對(duì)象緩存的不一致,來(lái)實(shí)現(xiàn)對(duì)象寫(xiě)入緩存。原理就是每次頁(yè)面展示的時(shí)候,只更新緩存中的對(duì)象,頁(yè)面顯示的時(shí)候優(yōu)先讀取緩存,但是不更新數(shù)據(jù)庫(kù),讓緩存保持不一致,積累到n次,直接更新一次數(shù)據(jù)庫(kù),但繞過(guò)緩存過(guò)期操作。具體的做法可以參考blog.rb :

# blog viewer hit counter  def increment_view_count    increment(:view_count)        # add view_count += 1    write_second_level_cache      # update cache per hit, but do not touch db                                  # update db per 10 hits    self.class.update_all({:view_count => view_count}, :id => id) if view_count % 10 == 0  end

increment(:view_count)增加view_count計(jì)數(shù),關(guān)鍵代碼是第2行write_second_level_cache,更新view_count之后直接寫(xiě)入緩存,但不更新數(shù)據(jù)庫(kù)。累計(jì)10次點(diǎn)擊,再更新一次數(shù)據(jù)庫(kù)相應(yīng)的字段。另外還要注意,如果blog對(duì)象不是通過(guò)主鍵查詢(xún),而是通過(guò)查詢(xún)語(yǔ)句構(gòu)造的,要優(yōu)先讀取一次緩存,保證頁(yè)面點(diǎn)擊次數(shù)的顯示一致性,因此 _blog.erb 這個(gè)頁(yè)面模版文件開(kāi)頭有這樣一段代碼:

<%     # read view_count from model cache if model has been cached.    view_count = blog.view_count    if b = Blog.read_second_level_cache(blog.id)      view_count = b.view_count    end %>

采用對(duì)象寫(xiě)入緩存的設(shè)計(jì)模式,就可以非常容易的實(shí)現(xiàn)寫(xiě)入操作的緩存,在這個(gè)例子當(dāng)中,我們僅僅增加了一行緩存寫(xiě)入代碼,而這個(gè)時(shí)間開(kāi)銷(xiāo)大約是1ms,就可以實(shí)現(xiàn)文章實(shí)時(shí)點(diǎn)擊計(jì)數(shù)功能,是不是非常簡(jiǎn)單和巧妙?實(shí)際上我們也可以使用這種設(shè)計(jì)模式實(shí)現(xiàn)很多數(shù)據(jù)庫(kù)寫(xiě)入的緩存功能。

常用的ORM緩存設(shè)計(jì)模式就是以上的幾種,本質(zhì)上都是非常簡(jiǎn)單的編程技巧,代碼的增加量和復(fù)雜度也非常低,只需要很少的代碼就可以實(shí)現(xiàn),但是在實(shí)際應(yīng)用當(dāng)中,特別是當(dāng)數(shù)據(jù)量很龐大,訪問(wèn)量很高的時(shí)候,可以發(fā)揮驚人的效果。我們實(shí)際的系統(tǒng)當(dāng)中,緩存命中次數(shù):SQL查詢(xún)語(yǔ)句,一般都是5:1左右,即每次向數(shù)據(jù)庫(kù)查詢(xún)一條SQL,都會(huì)在緩存當(dāng)中命中5次,數(shù)據(jù)主要都是從緩存當(dāng)中得到,而非來(lái)自于數(shù)據(jù)庫(kù)了。

其他緩存的使用技巧

還有一些并非ORM特有的緩存設(shè)計(jì)模式,但是在Web應(yīng)用當(dāng)中也比較常見(jiàn),簡(jiǎn)單提及一下:

用數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)的緩存

在我這個(gè)網(wǎng)站當(dāng)中,每篇文章都標(biāo)記了若干tag,而tag關(guān)聯(lián)關(guān)系都是保存到數(shù)據(jù)庫(kù)里面的,如果每次顯示文章,都需要額外查詢(xún)關(guān)聯(lián)表獲取tag,顯然會(huì)非常消耗數(shù)據(jù)庫(kù)。在我使用的acts-as-taggable-on插件中,它在blogs表當(dāng)中添加了一個(gè)cached_tag_list字段,保存了該文章標(biāo)記的tag。當(dāng)文章被修改的時(shí)候,會(huì)自動(dòng)相應(yīng)更新該字段,避免了每次顯示文章的時(shí)候都需要去查詢(xún)關(guān)聯(lián)表的開(kāi)銷(xiāo)。

HTTP客戶(hù)端緩存

基于資源協(xié)議實(shí)現(xiàn)的HTTP客戶(hù)端緩存也是一種非常有效的緩存設(shè)計(jì)模式,我在2009年寫(xiě)過(guò)一篇文章詳細(xì)的講解了:基于資源的HTTP Cache的實(shí)現(xiàn)介紹 ,所以這里就不再?gòu)?fù)述了。

用緩存實(shí)現(xiàn)計(jì)數(shù)器功能

這種設(shè)計(jì)模式有點(diǎn)類(lèi)似于對(duì)象寫(xiě)入緩存,利用緩存寫(xiě)入的低開(kāi)銷(xiāo)來(lái)實(shí)現(xiàn)高性能計(jì)數(shù)器。舉一個(gè)例子:用戶(hù)登錄為了避免遭遇密碼暴力破解,我限定了每小時(shí)每IP只能?chē)L試登錄5次,如果超過(guò)5次,拒絕該IP再次嘗試登錄。代碼實(shí)現(xiàn)很簡(jiǎn)單,如下:

post :login, :map => '/login' do    login_tries = APP_CACHE.read("#{CACHE_PREFIX}/login_counter/#{request.ip}")    halt 403 if login_tries && login_tries.to_i > 5  # reject ip if login tries is over 5 times    @account = Account.new(params[:account])    if login_account = Account.authenticate(@account.email, @account.password)      session[:account_id] = login_account.id      redirect url(:index)    else     # retry 5 times per one hour     APP_CACHE.increment("#{CACHE_PREFIX}/login_counter/#{request.ip}", 1, :expires_in => 1.hour)      render 'home/login'   end end

等用戶(hù)POST提交登錄信息之后,先從緩存當(dāng)中取該IP嘗試登錄次數(shù),如果大于5次,直接拒絕掉;如果不足5次,而且登錄失敗,計(jì)數(shù)加1,顯示再次嘗試登錄頁(yè)面。

“Web應(yīng)用的緩存設(shè)計(jì)模式舉例分析”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

分享文章:Web應(yīng)用的緩存設(shè)計(jì)模式舉例分析
文章路徑:http://bm7419.com/article18/geijdp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃網(wǎng)站制作、關(guān)鍵詞優(yōu)化企業(yè)網(wǎng)站制作、響應(yīng)式網(wǎng)站、網(wǎng)站維護(hù)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

小程序開(kāi)發(fā)