10分鐘內學會快取機制

這篇文章我想和你分享如何透過快取來提升你的 API 效能,因為每一位後端工程師都會被要求要提升系統的效能
請你看看左邊這張圖,這是電商常見的一種行銷操作
假如行銷人員希望這張圖表是變動的,需要用程式動態算出商品排名的話,該怎麼做呢?
作法需要結合 item_order 中介表格以及 items 商品表格來查詢出銷售量最多的 10 個商品,
算起來最少就要做兩張表格的查詢,而且還沒算到中間的商業邏輯運算
如果需要用到 orders 表格的資料,那 loading 就變得更多,而且還是每一次有人請求這張表就要做這些計算,這就是方案A的做法
但其實也可以考慮方案B ,就是直接從快取去抓取資料,就像是數學考試時看到題目不用算
直接把背好的答案寫上去,就可以節省掉計算所花費的時間囉
那究竟該如何去實作快取呢?答案就是結合方案A與方案B
當快取不存在資料時,先透過方案A來算出答案,答案出來後存入快取中。之後當有其他人需要這份資料時,就一律透過快取來提供,直到快取過期或者是被刪除為止
再繼續討論更多實作的細節之前,我想先和你聊另一個話題
就是 Laravel 快取技術的底層是由什麼實作出來的?
其實 Laravel 框架內建支援很多種的快取機制,這邊我想要向你說明最常見的三種
第一種是透過檔案來儲存,驅動名稱為 file,其實這也是 Laravel 所內建的快取機制
如果你不做任何改動的使用快取,使用的就是 file 驅動
第二種是透過資料庫表格來儲存,驅動名稱為 database
經過我的測試它的效能比檔案來的更高,但你需要先生成快取表格才能夠啟用喔
資料庫快取啟動流程
步驟1.建立 migration
php artisan cache:table
步驟2.執行 migrate
php artisan migrate
第三種是透過 Redis 服務來儲存,驅動名稱為 redis
Redis 是一種知名的鍵值對形式的資料庫,廣泛地用於中大型系統的快取服務,因為需要花費成本才能去架設使用,多用於已經上線且對效能有更多要求的系統上
那究竟該如何切換成其他驅動呢?答案非常的簡單!
你看一下快取設定檔裡的 default 設定,它會先去看隱私設定檔,如果不存在就指定為 file
而 file 的檔案路徑就列在下方,如果你有興趣的可以看一下
//config\cache.php
'default' => env('CACHE_DRIVER', 'file'),
'stores' => [
...
'database' => [
'driver' => 'database',
'table' => 'cache',
'connection' => null,
'lock_connection' => null,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
]
]
而 Laravel 新建專案的 .env 檔案裡頭,CACHE_DRIVER的 值就是 file,如果你想要改成其他驅動
只要改這個鍵的值即可
//.env
CACHE_DRIVER=database
效能優化實測
為了實地的了解快取能帶來的效能提升,我做了一些測試,歡迎一起來看看這些測試的數據
未使用快取圖
導入檔案(file)來進行快取,這一次有百分之九十的請求都是在 78 微秒內完成,
縮短了大約 22 % 的請求時間
導入資料庫(database)來進行快取,有百分之九十的請求都是在 65 微秒內完成,縮短了大約 35 % 的請求時間
如果你好奇我是怎麼進行測試的,我的測試作法如下:
步驟 1
修改 .env 檔案
//.env
APP_DEBUG=false
步驟2
開啟 Terminal ,執行以下指令
ab -t 10 -c 10 {應用首頁網址}
Cache Facade 介紹
這邊我來介紹一些比較常用的函式
has()
用來判斷某快取資料是否存在,你必須要確認該快取存在才能夠取用
forever()
用來永久地將資料存入快取中,該資料永遠不會過期,但你可以手動的清除這些資料
get()
用來取得某快取資料
flush()
可以一次性的清除所有自訂的快取資料
導入快取程式碼
//app\Http\Controllers\SiteController.php
public function renderNewsPage(Request $request)
{
//判斷快取是否具有該資料
if (!Cache::has('staticPageCache_news')) {
//沒有的話,就進行查詢或運算
$query = Article::where('status', 'published');
$cgy_id = null;
$keyword = null;
if ($request->has('cgy_id')) {
$cgy_id = $request->cgy_id;
$query = $query->where('cgy_id', $cgy_id);
} else {
$ids = Cgy::find(21)->getChildIds();
$query = $query->whereIn('cgy_id', $ids);
}
$articles = $query->paginate(3);
$cgies = Cgy::where('parent_id', 21)->enabled(true)->get();
$data = compact('articles', 'cgies', 'cgy_id', 'keyword');
//將資料永久的存入快取之中
Cache::forever('staticPageCache_news', $data);
}
//將資料從快取中取出
return view('news', Cache::get('staticPageCache_news'));
}
程式碼大概分為四段:
第一段先判斷快取是否存在
第二段如果快取不存在就進行查詢或運算
第三段在查詢或運算後將結果存入快取
第四段將快取資料傳入到視圖內
清除快取程式碼
//app\Http\Controllers\SiteController.php
//清除頁面快取
public function clearPageCache()
{
//只有管理員才可以清除快取
if (Auth::user()->role->name == 'admin') {
Cache::flush();
return '頁面 Cache 已經清除完畢';
} else {
return redirect(url('/'));
}
}
//routes\web.php
Route::namespace('App\Http\Controllers')->group(function () {
...
Route::get('/clearcache', 'SiteController@clearPageCache');
});
導入快取之後,所有的查詢運算都只會進行一次,之後都是轉由快取來提供,最終達成我們想要的系統效能提升
但有個問題是當資料被變更時,前端得到的依然會是快取的資料,因此需要搭配手動清除快取的機制
我的作法是會設計一個只提供給管理員去使用的路,當資料變更時就清除所有的快取,就能夠確保前端可以得到最新的資料囉