摘要:這三行代碼,則是在緩存命中時(shí),直接讀取緩存內(nèi)容并且返回。通過上面的源碼分析,我們可以知道,當(dāng)緩存失效時(shí),方法會直接將其代碼塊中的代碼的返回值不加判斷地寫入緩存,并且返回該返回值。
Rails 中的 active_support 組件主要基于 Rails 需要提供了很多非常有用的基礎(chǔ)工具以及對 Ruby 內(nèi)置類進(jìn)行擴(kuò)展。其中的 cache 模塊主要提供了 Rails 中底層緩存的定義以及簡單實(shí)現(xiàn)。今天要跟大家探討的是之前在使用此模塊所遇到的一個(gè)坑,有興趣學(xué)習(xí)其基本用法的可以點(diǎn)擊以下兩個(gè)鏈接:
Rails Guides: ActiveSupport::Cache::Store
Rails API: ActiveSupport::Cache::Store
從 ActiveSupport::Cache::Store#fetch 聊起之前在實(shí)現(xiàn)一個(gè)需要從外部服務(wù)請求數(shù)據(jù)的功能時(shí),處于性能考慮,我在代碼中使用了緩存,并且設(shè)置緩存失效時(shí)間為 7 天,示例代碼如下:
def read_external_service(params) # 這段代碼稍微解釋下: # 當(dāng)緩存命中時(shí),則直接讀取緩存,如果無期待緩存,則通過 HTTP 向外請求結(jié)果,并且將結(jié)果 # 緩存下來,這樣子,當(dāng)下次繼續(xù)調(diào)用時(shí),則可直接返回緩存內(nèi)容,而無需重復(fù)向外請求 # Rails.cache.fetch "example_cache_key_here", expires_in: 7.days do response = HTTParty.get "https://example.com/example/request/path" JSON.parse(response.body)["data"] end end
上面的代碼其實(shí)不復(fù)雜,核心代碼就是使用了 ActiveSupport::Cache::Store#fetch 方法。
一切都很正常地運(yùn)行著,直到有一天,線上系統(tǒng)不斷報(bào)警,出錯(cuò)原因就是這段代碼總是返回 nil ,而調(diào)用者又因?yàn)闆]有判斷 nil 值,就會出現(xiàn) undefined method "xxx" for nil:NilClass 錯(cuò)誤。在 debug 時(shí),我嘗試了直接調(diào)用外部服務(wù)接口,發(fā)現(xiàn)請求都有正確返回?cái)?shù)據(jù),不可能返回 nil 啊,難道是緩存了 nil 值?下面就直接通過代碼驗(yàn)證一下!
[1] pry(main)> require "active_support" => true [2] pry(main)> cache = ActiveSupport::Cache::MemoryStore.new => <#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}> [3] pry(main)> cache.read :nil_value => nil [4] pry(main)> cache.exist? :nil_value => false [5] pry(main)> cache.fetch :nil_value do [5] pry(main)* nil # this `nil` value will be cached [5] pry(main)* end => nil [6] pry(main)> cache.read :nil_value => nil [7] pry(main)> cache.exist? :nil_value => true
看吧, fetch 方法確實(shí)會緩存 nil 值(通過 exist? 方法可以判斷是否緩存了指定的 key ),所以系統(tǒng)出錯(cuò)原因就清晰了:在某次代碼執(zhí)行中,我的緩存剛好失效了,所以系統(tǒng)向外部發(fā)送了請求,恰巧這時(shí)候外部系統(tǒng)因?yàn)楣收匣蛘咂渌赡茉?,沒有返回期待數(shù)據(jù),導(dǎo)致代碼中最終緩存了 nil 值,在接下來的時(shí)間里,雖然外部系統(tǒng)可能恢復(fù)了正確服務(wù),可是這時(shí)候因?yàn)槲覀兊南到y(tǒng)已經(jīng)緩存了 nil值,所以在每次調(diào)用時(shí)都返回緩存的 nil,而不是重新請求正確結(jié)果,導(dǎo)致最后不停的報(bào)錯(cuò)告警。
這里插播一句,通過后來仔細(xì)查閱文檔,才發(fā)現(xiàn)文檔里已經(jīng)注明:
Nil values can be cached.
╮(╯▽╰)╭ 怪我咯~
解決方案意識到這個(gè)問題之后,解決思路簡單粗暴,就是在可能返回 nil 值的地方放棄寫入緩存:
def read_external_service(params) cache_key = "example_cache_key_here" result = Rails.cache.read(cache_key) # 緩存命中,且內(nèi)容不為 nil ,直接返回緩存內(nèi)容 return result if result.present? # 緩存失效,只能重新請求了~ response = HTTParty.get "https://example.com/example/request/path" result = JSON.parse(response.body)["data"] # 請求結(jié)果正確,寫入緩存;否則,放棄之~~~ Rails.cache.write(cache_key, result, expires_in: 7.days) if result.present? result end
呃~~~雖然解決問題了,可是,就為了告訴系統(tǒng)不要相信 nil,就寫得這么繁瑣,好么?好么?好么?
踏上閱讀源碼之路我嘗試搜索了 #fetch 方法是否有支持比如 reject_nil 這樣的 option,可惜的是,沒有!可是真的沒有嗎?我不信!看源碼去!
首先還是拜訪下 ActiveSupport::Cache::Store 這個(gè)類啦,它可是所有緩存實(shí)現(xiàn)類的抽象類,別問我抽象類是什么,就是它明明只說話不干活,但是其他干活的都得向它看齊!好啦,說人話,其實(shí)就是說,我們在調(diào)用 Rails.cache.read、Rails.cache.fetch 等讀寫方法時(shí),這些方法都是在 ActiveSupport::Cache::Store 中定義的,但是它只定義邏輯,而實(shí)際底層的讀寫實(shí)現(xiàn),則都是交由其各種子類實(shí)現(xiàn)的,比如前面的 ActiveSupport::Cache::MemoryStore。
首先讓我們來看看 fetch方法的全部內(nèi)容:
def fetch(name, options = nil) if block_given? options = merged_options(options) key = namespaced_key(name, options) instrument(:read, name, options) do |payload| cached_entry = read_entry(key, options) unless options[:force] payload[:super_operation] = :fetch if payload entry = handle_expired_entry(cached_entry, key, options) if entry payload[:hit] = true if payload get_entry_value(entry, name, options) else payload[:hit] = false if payload save_block_result_to_cache(name, options) { |_name| yield _name } end end else read(name, options) end
從代碼中可以看到,當(dāng) #fetch 方法調(diào)用時(shí)沒有傳遞 block 的話,它本質(zhì)上就是 read 方法的別名而已。而當(dāng)調(diào)用時(shí)傳遞了 block 的話,即如我前面的示例代碼,讓我們把代碼分開看下:
cached_entry = read_entry(key, options) unless options[:force] payload[:super_operation] = :fetch if payload entry = handle_expired_entry(cached_entry, key, options)
它首先判斷是否設(shè)置了 force 選項(xiàng),如果有,則不讀取緩存,由此模擬緩存強(qiáng)制失效;如果未設(shè)置 force 選項(xiàng)或者該選項(xiàng)不等于 true value,則嘗試讀取緩存,并且調(diào)用 handle_expired_entry判斷緩存是否仍舊有效。
if entry payload[:hit] = true if payload get_entry_value(entry, name, options)
這三行代碼,則是在緩存命中時(shí),直接讀取緩存內(nèi)容并且返回。
else payload[:hit] = false if payload save_block_result_to_cache(name, options) { |_name| yield _name } end
else 的代碼則表示,在緩存無命中時(shí), #fetch 代碼直接調(diào)用 #save_block_result_to_cache 方法,并且向其傳遞了一個(gè) block,這個(gè) block 沒有干別的事情,它只會執(zhí)行我們傳遞給 #fetch 方法的 block,讓我們接著往下看看相關(guān)的實(shí)現(xiàn):
def save_block_result_to_cache(name, options) result = instrument(:generate, name, options) do |payload| yield(name) end write(name, result, options) result end
可以看到,#save_block_result_to_cache 方法首先執(zhí)行傳遞進(jìn)來的代碼塊,實(shí)際上也就是我們期待在緩存失效時(shí)執(zhí)行的代碼,而在獲得執(zhí)行結(jié)果 result 后,方法通過調(diào)用 #write 方法將結(jié)果寫入緩存,最后將 result 返回。
通過上面的源碼分析,我們可以知道,當(dāng)緩存失效時(shí),#fetch 方法會直接將其代碼塊中的代碼的返回值不加判斷地寫入緩存,并且返回該返回值。這里,或許我們可以做點(diǎn)什么,來實(shí)現(xiàn)我們想要支持 :reject_nil 的需求?
支持 :reject_nil option為了支持 :reject_nil,我們只需要在寫入緩存前判斷是否真的需要 nil 值即可,于是我們只需要在 #save_block_result_to_cache 中加入 #write 的前置條件:
def save_block_result_to_cache(name, options) result = instrument(:generate, name, options) do |payload| yield(name) end # options[:reject_nil] && result.nil? 作為前置條件 write(name, result, options) unless result.nil? && options[:reject_nil] result end
話不多說,讓我們來重新試驗(yàn)一番:
[1] pry(main)> require "active_support" => true [2] pry(main)> cache = ActiveSupport::Cache::MemoryStore.new => <#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}> [3] pry(main)> cache.fetch :nil_key1 do [3] pry(main)* nil [3] pry(main)* end => nil [4] pry(main)> cache.exist? :nil_key1 => true [5] pry(main)> cache.fetch :nil_key2, reject_nil: true do [5] pry(main)* nil [5] pry(main)* end => nil [6] pry(main)> cache.exist? :nil_key2 => false
可以看到,當(dāng)我們調(diào)用 #fetch 方法時(shí),如果沒有傳遞 reject_nil: true,則 #fetch 方法會默認(rèn)緩存 nil 值;而如果我們設(shè)置 reject_nil: true 的話,則 #fetch 就會放棄寫入 nil 值到緩存中。試驗(yàn)成功!?。?/p>
基于這樣的實(shí)現(xiàn),我的代碼就又可以改為如下了:
def read_external_service(params) # 所有改動只是加了一個(gè) `reject_nil: true`,多方便,媽媽再也不用擔(dān)心我掉到坑里去了 Rails.cache.fetch "example_cache_key_here", expires_in: 7.days, reject_nil: true do response = HTTParty.get "https://example.com/example/request/path" JSON.parse(response.body)["data"] end end
待會去給 Rails 提交 Pull Request 去 O(∩_∩)O~~
總結(jié)緩存是好個(gè)東西,用得好能夠讓應(yīng)用性能表現(xiàn)突飛猛進(jìn)
要注意緩存寫入的邊界條件,要注意避免緩存了空值,但也并非所有空值都不能緩存(比如有些接口確實(shí)就是有可能返回空值嘛),具體看業(yè)務(wù),沒有絕對的要與不要,反正 :reject_nil 給你了,看你要不要
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/61765.html
摘要:云計(jì)算提供商提供的云服務(wù)在出現(xiàn)中斷時(shí),行業(yè)廠商和用戶似乎都在學(xué)習(xí)如何設(shè)計(jì)本地云冗余,管理人員希望將這些教訓(xùn)應(yīng)用于傳統(tǒng)的虛擬化數(shù)據(jù)中心冗余。云計(jì)算冗余并不完美,停機(jī)中斷提供了經(jīng)驗(yàn)教訓(xùn)高度冗余的系統(tǒng)不會免受性能下降的影響。很多公共云提供商在日常運(yùn)營中通常不可避免地遭遇災(zāi)難性中斷,而IT管理人員需要從云平臺的故障和教訓(xùn)中學(xué)習(xí),并將其應(yīng)用到內(nèi)部基礎(chǔ)設(shè)施當(dāng)中。云平臺(尤其是大型公共云平臺)具有多種冗余...
摘要:尤其是云計(jì)算監(jiān)控,已經(jīng)引起了人們的廣泛關(guān)注。云計(jì)算監(jiān)控具有很多應(yīng)用,但作為一種單獨(dú)的解決方案,它充滿了危險(xiǎn)。企業(yè)必須注意這些危險(xiǎn),而不是認(rèn)為可以單獨(dú)依靠云計(jì)算監(jiān)控。數(shù)據(jù)中心和IT運(yùn)營經(jīng)理長期以來一直認(rèn)為,僅從防火墻后面進(jìn)行監(jiān)控并不能了解最終用戶是否享受快速可靠的數(shù)字體驗(yàn)。這是因?yàn)榉阑饓χ膺€有大量的外部元素——第三方服務(wù)、ISP、CDN等等,這可能會影響最終用戶在最后一英里的最終體驗(yàn)。最終用...
摘要:在客戶端基于圖片格式的流量優(yōu)化上這篇文章中,已經(jīng)介紹了格式圖片的下載使用,僅僅只有這樣還遠(yuǎn)遠(yuǎn)不夠,還需要對已經(jīng)下載的圖片數(shù)據(jù)進(jìn)行緩存。二圖片緩存關(guān)于的緩存,系統(tǒng)提供了一個(gè)類,。而且,既然是全局影響,肯定要用包起來,防止誤傷其他緩存。 在iOS 客戶端基于 WebP 圖片格式的流量優(yōu)化(上)這篇文章中,已經(jīng)介紹了WebP格式圖片的下載使用,僅僅只有這樣還遠(yuǎn)遠(yuǎn)不夠,還需要對已經(jīng)下載的圖片數(shù)...
摘要:函數(shù)總共操作有兩步從緩存中查詢值,如果查到則返回如果為從緩存中查詢到則回調(diào)回調(diào)函數(shù)。回調(diào)函數(shù)會將從磁盤上獲得到塊信息存儲到緩存中并返回該塊的信息?;卣{(diào)函數(shù)實(shí)際上調(diào)取的是下的,它會從磁盤中獲取信息并返回。 作者:Derek 簡介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc......
閱讀 1978·2021-11-22 15:33
閱讀 3009·2021-11-18 10:02
閱讀 2622·2021-11-08 13:16
閱讀 1633·2021-10-09 09:57
閱讀 1378·2021-09-30 09:47
閱讀 2013·2019-08-29 13:05
閱讀 3076·2019-08-29 12:46
閱讀 1013·2019-08-29 12:19