[分享]總結(jié)Web應(yīng)用中常用的各種Cache

總結(jié)Web應(yīng)用中常用的各種Cache

來源:Ruby China

網(wǎng)址:https://ruby-china.org/topics/19389

cache是提高應(yīng)用性能重要的一個環(huán)節(jié)吟吝,寫篇文章總結(jié)一下用過的各種對于動態(tài)內(nèi)容的cache菱父。

文章以Nginx,Rails剑逃,Mysql浙宜,Redis作為例子,換成其他web服務(wù)器蛹磺,語言粟瞬,數(shù)據(jù)庫,緩存服務(wù)都是類似的萤捆。

以下是3層的示意圖裙品,方便后續(xù)引用:

30.jpg

1.客戶端緩存

一個客戶端經(jīng)常會訪問同一個資源,比如用瀏覽器訪問網(wǎng)站首頁或查看同一篇文章俗或,或用app訪問同一個api市怎,如果該資源和他之前訪問過的沒有任何改變,就可以利用http規(guī)范中的304 Not Modified 響應(yīng)頭(http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 )辛慰,直接用客戶端的緩存区匠,而無需在服務(wù)器端再生成一次內(nèi)容。

在Rails里面內(nèi)置了fresh_when這個方法帅腌,一行代碼就可以完成:

class ArticlesController
  def show
    @article = Article.find(params[:id])
    fresh_when :last_modified => @article.updated_at.utc, :etag => @article
  endend

下次用戶再訪問的時候驰弄,會對比request header里面的If-Modified-Since和If-None-Match麻汰,如果相符合,就直接返回304戚篙,而不再生成response body五鲫。

但是這樣會遇到一個問題,假設(shè)我們的網(wǎng)站導(dǎo)航有用戶信息已球,一個用戶在未登陸專題訪問了一下臣镣,然后登陸以后再訪問,會發(fā)現(xiàn)頁面上顯示的還是未登陸狀態(tài)智亮∫淠常或者在app訪問一篇文章,做了一下收藏阔蛉,下次再進入這篇文章弃舒,還是顯示未收藏狀態(tài)。解決這個問題的方法很簡單状原,將用戶相關(guān)的變量也加入到etag的計算里面:

 fresh_when :etag => [@article.cache_key, current_user.id]
    fresh_when :etag => [@article.cache_key, current_user_favorited]

另外提一個坑聋呢,如果nginx開啟了gzip,對rails執(zhí)行的結(jié)果進行壓縮颠区,會將rails輸出的etag header干掉削锰,nginx的開發(fā)人員說根據(jù)rfc規(guī)范,對proxy_pass方式處理必須這樣(因為內(nèi)容改變了)毕莱,但是我個人認(rèn)為沒這個必要器贩,于是用了粗暴的方法,直接將src/http/modules/ngx_http_gzip_filter_module.c這個文件里面的這行代碼注釋掉朋截,然后重新編譯nginx:

//ngx_http_clear_etag(r);

或者你可以選擇不改變nginx源代碼蛹稍,將gzip off掉,將壓縮用Rack中間件來處理:

config.middleware.use Rack::Deflater

除了在controller里面指定fresh_when以外部服,rails框架默認(rèn)使用Rack::ETag middleware唆姐,它會自動給無etag的response加上etag,但是和fresh_when相比廓八,自動etag能夠節(jié)省的只是客戶端時間奉芦,服務(wù)器端還是一樣會執(zhí)行所有的代碼,用curl來對比一下剧蹂。

Rack::ETag自動加入etag:

curl -v http://localhost:3000/articles/1
< Etag: "bf328447bcb2b8706193a50962035619"
< X-Runtime: 0.286958
curl -v http://localhost:3000/articles/1 --header 'If-None-Match: "bf328447bcb2b8706193a50962035619"'
< X-Runtime: 0.293798

用fresh_when:

curl -v http://localhost:3000/articles/1 --header 'If-None-Match: "bf328447bcb2b8706193a50962035619"'
< X-Runtime: 0.033884

2. Nginx緩存

有一些資源可能會被調(diào)用很多声功,又無關(guān)用戶狀態(tài),并且很少改變国夜,比如新聞app上的列表api减噪,購物網(wǎng)站上ajax請求分類菜單短绸,可以考慮用Nginx來做緩存车吹。

主要有2種實現(xiàn)方法:

  • A. 動態(tài)請求靜態(tài)文件化

在rails請求完成以后筹裕,將結(jié)果保存成靜態(tài)文件,后續(xù)請求就會直接由nginx提供靜態(tài)文件內(nèi)容窄驹,用after_filter來實現(xiàn)一下:

class CategoriesController < ActionController::Base
  after_filter :generate_static_file, :only => [:index]

  def index
    @categories = Category.all
  end

  def generate_static_file
    File.open(Rails.root.join('public', 'categories'), 'w') do |f|
      f.write response.body
    end
  endend

另外我們需要在任何分類更新的時候朝卒,刪除掉這個文件,避免緩存不刷新的問題:

class Category < ActiveRecord::Base
  after_save :delete_static_file
  after_destroy :delete_static_file

  def delete_static_file
    File.delete Rails.root.join('public', 'categories')
  endend

Rails 4之前乐埠,處理這種生成靜態(tài)文件緩存可以用內(nèi)置的caches_page抗斤, rails 4之后變成了一個獨立gem actionpack-page_caching,和手工代碼對比一下丈咐,

class CategoriesController < ActionController::Base
  caches_page :index

  def update
    #...
    expire_page action: 'index'
  endend

如果只有一臺服務(wù)器瑞眼,這個方法簡單又實用,但是如果有多臺服務(wù)器棵逊,就會出現(xiàn)更新分類只能刷新自己本身這臺服務(wù)器緩存的問題伤疙,可以用nfs來共享靜態(tài)資源目錄解決,或者用第2種:

  • B. 靜態(tài)化到集中緩存服務(wù)

首先我們得讓Nginx有直接訪問緩存的能力:

upstream redis {
    server redis_server_ip:6379;
  }

  upstream ruby_backend {
    server unicorn_server_ip1 fail_timeout=0;
    server unicorn_server_ip2 fail_timeout=0;
  }

  location /categories {
    set $redis_key $uri;
    default_type   text/html;
    redis_pass redis;
    error_page 404 = @httpapp;
  }

  location @httpapp {
    proxy_pass http://ruby_backend;
  }

Nginx首先會用請求的uri作為key去redis里面獲取辆影,如果獲取不到(404)就轉(zhuǎn)發(fā)給unicorn進行處理徒像,然后改寫generate_static_file和delete_static_file方法:

redis_cache.set('categories', response.body)
  redis_cache.del('categories')

這樣除了集中管理以外,還能夠設(shè)置緩存的失效時間蛙讥,對于一些更新無時效性要求的數(shù)據(jù)锯蛀,就可以不用處理刷新機制,簡單地固定時間刷新一次:

redis_cache.setex('categories', 3.hours.to_i, response.body)

3. 整頁緩存

Nginx緩存在處理帶參數(shù)資源或者有用戶狀態(tài)的請求時候次慢,就非常難以處理旁涤,這個時候可以用到整頁緩存。

比如說分頁請求列表经备,我們可以將page參數(shù)加入到cache_path:

class CategoriesController
  caches_action :index, :expires_in => 1.day, :cache_path => proc {"categories/index/#{params[:page].to_i}"}end

比如說我們只需要針對rss輸出進行緩存8小時:

class ArticlesController
  caches_action :index, :expires_in => 8.hours, :if => proc {request.format.rss?}end

再比如說對于非登陸用戶拭抬,我們可以緩存首頁:

class HomeController
  caches_action :index, :expires_in => 3.hours, :if => proc {!user_signed_in?}end

4. 片段緩存

如果說前面2種緩存能夠用到的場景有限,那么片段緩存是適用性最廣的侵蒙。

場景1:我們需要在每個頁面一段廣告代碼造虎,用來顯示不同廣告,如果沒有使用片段緩存纷闺,那么每個頁面都會要去查詢廣告的代碼算凿,并且花費一定時間去生成html代碼:

- if advert = Advert.where(:name => request.controller_name + request.action_name, :enable => true).first
  div.ad
    = advert.content

加了片段緩存以后,就可以少去這個查詢:

- cache "adverts/#{request.controller_name}/#{request.action_name}", :expires_in => 1.day do
  - if advert = Advert.where(:name => request.controller_name + request.action_name, :enable => true).first
    div.ad
      = advert.content

場景2:閱讀文章犁功,文章的內(nèi)容可能比較長時間都不會改變氓轰,經(jīng)常變化可能是文章評論,就可以對文章主體部分加上片段緩存:

- cache "articles/#{@article.id}/#{@article.updated_at.to_i}" do
  div.article
    = @article.content.markdown2html

場景3:復(fù)雜頁面結(jié)構(gòu)的生成

數(shù)據(jù)結(jié)構(gòu)比較復(fù)雜的頁面浸卦,在生成的時候避免不了大量的查詢和html渲染署鸡,用片段緩存,可以將這部分時間大大地節(jié)約,以我們網(wǎng)站游記頁面 http://chanyouji.com/trips/109123 (請允許小小地打個廣告靴庆,帶點流量)來說:

需要獲取天氣數(shù)據(jù)时捌,照片數(shù)據(jù),文本數(shù)據(jù)等炉抒,同時還要生成meta奢讨,keyword等seo數(shù)據(jù),而這些內(nèi)容又是和其他動態(tài)內(nèi)容交叉焰薄,片段緩存就可以分開多個:

- cache "trips/show/seo/#{@trip.fragment_cache_key}", :expires_in => 1.day do
  title #{trip_name @trip}
  meta name="description" content="..."
  meta name="keywords" content="..."

body
  div
    ...
- cache "trips/show/viewer/#{@trip.fragment_cache_key}", :expires_in => 1.day do
  - @trip.eager_load_all

小貼士拿诸,我在trip對象里面加了一個eager_load_all方法,緩存沒有命中的時候塞茅,查詢的時候避免出現(xiàn)n+1問題:

def eager_load_all
    ActiveRecord::Associations::Preloader.new([self], {:trip_days => [:weather_station_data, :nodes => [:entry, :notes => [:photo, :video, :audio]]]}).run
  end

小技巧1:帶條件的片段緩存

和caches_action不同亩码,rails自帶的片段緩存是不支持條件的,比如說我們想未登陸用戶給他用片段緩存野瘦,而登陸用戶不使用蟀伸,寫起來就很麻煩,我們可以改寫一下helper就可以了:

 def cache_if (condition, name = {}, cache_options = {}, &block)
    if condition
      cache(name, cache_options, &block)
    else
      yield
    end
  end- cache_if !user_signed_in?, "xxx", :expires_in => 1.day do

小技巧2:關(guān)聯(lián)對象的自動更新

常使用對象update_at時間戳來作為cache key缅刽,可以在關(guān)聯(lián)對象上加上touch選項啊掏,自動更新關(guān)聯(lián)對象時間戳,比如我們可以在更新或者刪除文章評論的時候衰猛,自動個更新:

class Article
  has_many :commentsendclass Comment
  belongs_to :article, :touch => trueend

5. 數(shù)據(jù)查詢緩存

通常來說web應(yīng)用性能瓶頸都出現(xiàn)在DB IO上迟蜜,做好數(shù)據(jù)查詢緩存,減少數(shù)據(jù)庫的查詢次數(shù)啡省,可以極大提高整體響應(yīng)時間娜睛。

數(shù)據(jù)查詢緩存分2種:

  • A. 同一個請求周期內(nèi)的緩存

舉一個顯示文章列表的例子,輸出文章標(biāo)題和文章類別卦睹,對應(yīng)代碼如下

# controller
  def index
    @articles = Article.first(10)
  end# view- @articles.each do |article|
  h1 = article.name
  span = article.category.name

會發(fā)生10條類似的sql查詢:

SELECT `categories`.* FROM `categories` WHERE `categories`.`id` = ?

rails內(nèi)置了query cache (https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb )畦戒,在同一個請求周期內(nèi),如果沒有update/delete/insert的操作结序,會對相同的sql查詢進行緩存障斋,如果文章類別都是相同的話,真正去查詢數(shù)據(jù)庫只會有1次徐鹤。

如果文章類別都不一樣垃环,就會出現(xiàn)N+1查詢問題(常見的性能瓶頸),rails推薦的解決方法是用Eager Loading Associations ( http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations )

def index
    @articles = Article.includes(:category).first(10)
  end

查詢語句會變成

SELECT `categories`.* FROM `categories` WHERE `categories`.`id` in (?,?,?...)
  • B. 跨請求周期的緩存

同請求周期緩存所帶來性能優(yōu)化是很有限的返敬,很多時候我們需要用跨請求周期的緩存遂庄,將一些常用的數(shù)據(jù)(比如User model)緩存,對于active record來說劲赠,利用統(tǒng)一的查詢接口來fetch cache涛目,利用callback來expire cache秸谢,就很容易實現(xiàn),而且有一些現(xiàn)成的gem可以來用霹肝。

比如說 identity_cache ( https://github.com/Shopify/identity_cache )

class User < ActiveRecord::Base
  include IdentityCacheendclass Article < ActiveRecord::Base
  include IdentityCache
  cached_belongs_to :userend# 都會命中緩存User.fetch(1)Article.find(2).user

這個gem的優(yōu)點是代碼實現(xiàn)簡單钮追,cache設(shè)置靈活,也方便擴展阿迈,缺點是需要用不同的查詢方法名(fetch),以及額外的關(guān)系定義轧叽。

如果想在無數(shù)據(jù)緩存的應(yīng)用無縫加入緩存功能苗沧,推薦@hooopo 做的second_level_cache (https://github.com/hooopo/second_level_cache ) 。

class User < ActiveRecord::Base
  acts_as_cached(:version => 1, :expires_in => 1.week)end#還是使用find方法炭晒,就會命中緩存User.find(1)#無需額外用不一樣的belongs_to定義Article.find(2).user

實現(xiàn)原理是擴展了active record底層arel sql ast處理 (https://github.com/hooopo/second_level_cache/blob/master/lib/second_level_cache/arel/wheres.rb

它的優(yōu)點是無縫接入待逞,缺點是擴展比較困難,對于只獲取少量字段的查詢無法緩存网严。

6. 數(shù)據(jù)庫緩存

這6種緩存识樱,分布在客戶端到服務(wù)器端不同的位置,所能夠節(jié)約的時間也正好從多到少依次排列震束。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末怜庸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子垢村,更是在濱河造成了極大的恐慌割疾,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嘉栓,死亡現(xiàn)場離奇詭異宏榕,居然都是意外死亡,警方通過查閱死者的電腦和手機侵佃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門麻昼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人馋辈,你說我怎么就攤上這事抚芦。” “怎么了迈螟?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵燕垃,是天一觀的道長。 經(jīng)常有香客問我井联,道長卜壕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任烙常,我火速辦了婚禮轴捎,結(jié)果婚禮上鹤盒,老公的妹妹穿的比我還像新娘。我一直安慰自己侦副,他們只是感情好侦锯,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秦驯,像睡著了一般尺碰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音刊头,去河邊找鬼胶果。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼番枚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了损敷?” 一聲冷哼從身側(cè)響起葫笼,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拗馒,沒想到半個月后渔欢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡瘟忱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年奥额,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片访诱。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡垫挨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出触菜,到底是詐尸還是另有隱情九榔,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布涡相,位于F島的核電站哲泊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏催蝗。R本人自食惡果不足惜切威,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丙号。 院中可真熱鬧先朦,春花似錦缰冤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至刺彩,卻和暖如春迷郑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背创倔。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工嗡害, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人三幻。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像呐能,于是被迫代替她去往敵國和親念搬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容