小技巧:用二級快取提高快取命中率和記憶體使用效率
一直都沒找到二級快取在php中應用的比較好的資料和案例,由於範凱RobbinWeb 應用的快取設計模式和Hibernate二級快取的啟示,記下這篇二級快取在Eloquent ORM中的應用。
過程
比如部落格的首頁呼叫最新的20篇文章,相信不少同學在剛開始使用快取的時候,會寫下如下程式碼:
# 控制器 public function index() { $articles = Article::latestArticles(20); return view('articles.index', ['articles' => $articles]); } # 模型 class Article extends Model { public static function latestArticles($amount = 20) { return Cache::remember('articles:latest', 10, function () use ($amount) { return static::latest('id')->take($amount)->get(); }); } }
當然,模型中還能預載入每篇文章的分類,作者和tag資訊,看起來沒有任何問題,而且非常符合人類直覺。但是,放大到全站快取來看,還是有很大的改善空間。
首先,首頁快取的是一個包含20個article物件的集合,集合的每一個單獨的article物件除了在首頁出現,還會在分類、作者和tag等列表頁出現,還有文章詳情頁,而快取的集合資料沒辦法在這些頁面間共用,重複快取大量相同的article物件是對記憶體資源的很大浪費,要是article中的text欄位content沒有單獨拆分出去,記憶體浪費得就更嚴重了。
其次,不像詳情頁資料改動很少,首頁作為列表頁來說,更新頻率很高,設定的快取時間比較短,一般是分鐘級別,快取命中率並不高。
為了有效解決這兩個問題,二級快取就派上用場了,先說下自己對二級快取的理解。
一級快取可以看成是資料庫裡存的資料的一個映象,只不過把資料從資料庫搬到記憶體,一個key對應一條記錄。key一般為表的識別符號,比如key為articles:1存的value就是id=1的article物件。一級快取時間可以設得比較長,甚至forever也行,物件修改刪除時,只要刪除對應的key就行。
二級快取可以看成業務邏輯的快取,首頁最新20條文章 就屬於業務邏輯,只快取這20條文章的id,極大地節省了記憶體佔用。等需要用到具體的資料再去一級快取取,一級快取沒有才去查詢資料庫,由於都是主鍵查詢,不會造成表的描述,查詢效率非常高。即使二級快取很快過期,一級快取也不會失效。
個人覺得理解二級快取最難的是要接受n+1查詢這點,這個問題爭議很大,明明各種ORM為了避免n+1使用了預載入,我們反而要拋棄它。包括我當初閱讀範凱的《Web 應用的快取設計模式》也心存疑惑,直到去了解了Hibernate二級快取機制和自己在專案中的實踐發現,還真是他說的那樣。
拆分n+1條查詢的方式,看起來似乎非常違反大家的直覺,但實際上這是真理,我實踐經驗證明:資料庫伺服器的瓶頸往往是磁碟IO,而不是SQL併發數量。因此 拆分n+1條查詢本質上是以增加n條SQL語句為代價,簡化複雜SQL,換取資料庫伺服器磁碟IO的降低 當然這樣做以後,對於ORM來說,有額外的好處,就是可以高效的使用快取了。
使用二級快取來重構latestArticles方法
public static function latestArticles($amount = 20) { // 二級快取 $ids = Cache::remember('articles:latest:ids', 10, function () use ($amount) { return static::latest('id')->take($amount)->pluck('id'); }); return $ids->map(function ($id) { // 一級快取 return static::findById($id); }); } public static function findById($id) { return Cache::rememberForever("articles:{$id}", function () use ($id) { return static::find($id); }); }
除了返回Collection,還可以返回Generator。
public static function latestArticles($amount = 20) { // 二級快取 $ids = Cache::remember('articles:latest:ids', 10, function () use ($amount) { return static::latest('id')->take($amount)->pluck('id'); }); foreach ($ids as $id) { // 一級快取 yield static::findById($id); } }
吐槽
在專欄釋出介面,好不容易寫到這裡,結果手賤劃了下瀏覽器重新整理的滑鼠手勢,內心瞬間奔崩,砸電腦的心都有了 :rage:。雖然看提示有自動儲存,可只有標題被儲存了,內容還是空空如也。轉眼想想歷史上那些不小心毀了書稿寫出世界名著的作家,自己也只有硬著頭皮把內容回憶出來,所以後面的內容就一筆帶過了。
更新與刪除
一級快取的更新和刪除可能通過模型的updated和deleted事件來清除對應的快取。二級快取由於快取時間比較短,影響不大。
關聯關係
關聯模型的快取可能通過accessor來設定一個虛擬的屬性來設定,比如在Article模型與Content模型是一對一的關係。
在Article中:
// 一對一關聯 public function content() { return $this->hasOne(Content::class); } // contents表字段: article_id, body public function getContentAttribute() { return Cache::rememberForever("contents:{$this->id}", function () { return $this->content->body; }); }
寫在最後:歡迎留言討論,加關注,持續更新!