Phần lớn traffic của một website nội dung là người đọc ẩn danh mở trang chủ, trang danh mục và trang bài viết. Với mỗi request như vậy, Laravel vẫn boot framework, chạy hàng chục query rồi render Blade, dù kết quả gần như giống hệt lần trước. Tài liệu này hướng dẫn dùng Laravel ResponseCache (package spatie/laravel-responsecache) để lưu nguyên response HTML, đưa cache truy vấn sang Redis và giao queue worker cho Supervisor quản lý: cấu hình mẫu, cách xoá cache khi nội dung đổi, các lỗi hay gặp và checklist khi bật cache trên production.
1. Hai lớp cache trong một ứng dụng Laravel
Cần phân biệt hai thứ thường bị gọi chung là "cache":
| Cache truy vấn / dữ liệu | Cache response (Laravel ResponseCache) | |
|---|---|---|
| Lưu gì | Kết quả query, mảng dữ liệu (Cache::remember()) | Toàn bộ HTTP response: header và HTML đã render |
| Request trúng cache còn phải làm gì | Vẫn chạy controller, render view | Chỉ boot framework, chạy middleware rồi trả về ngay |
| Rủi ro | Dữ liệu cũ ở một khối nhỏ | Cả trang cũ, hoặc lộ nội dung của người này cho người khác |
Hai lớp này bổ sung cho nhau. Laravel ResponseCache cho lợi ích lớn nhất với trang công khai, còn cache truy vấn vẫn cần cho những chỗ không thể cache cả trang.
2. Laravel ResponseCache hoạt động thế nào
Laravel ResponseCache đăng ký một middleware. Khi request đi qua, middleware hỏi cache profile hai câu: request này có nên cache không, và response này có nên lưu không. Nếu có, response được serialize và ghi vào cache store của Laravel (file, Redis…) với khoá là hash của URL, method và một hậu tố do profile quyết định. Request sau cùng khoá nhận lại response đã lưu mà không chạy controller. Tài liệu đầy đủ ở trang hướng dẫn laravel-responsecache của Spatie.
Ví dụ dưới đây dùng Laravel 8. Cài package và đăng ký middleware dạng route middleware để bật theo từng route:
composer require spatie/laravel-responsecache:^7.4 predis/predis
php artisan vendor:publish --provider="Spatie\ResponseCache\ResponseCacheServiceProvider"
// app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'cache-response' => \Spatie\ResponseCache\Middlewares\CacheResponse::class,
'do-not-cache-response' => \Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class,
];predis/predis là Redis client viết bằng PHP thuần, dùng được ngay cả khi minion chưa cài extension phpredis. Nên ghim phiên bản theo major (như ^7.4), tránh @dev vì nó cho phép composer kéo bất kỳ bản nào ở lần update sau.
3. Cấu hình Laravel ResponseCache cho website nội dung
File config: bật tắt bằng biến môi trường
// config/responsecache.php (trích)
'enabled' => env('RESPONSE_CACHE_ENABLED', true),
'cache_profile' => App\Cache\SiteResponseCacheProfile::class,
'cache_lifetime_in_seconds' => env('RESPONSE_CACHE_LIFETIME', 60 * 5),
'add_cache_time_header' => env('RESPONSE_CACHE_DEBUG_HEADER', false),
'cache_store' => env('RESPONSE_CACHE_DRIVER', 'redis_response'),
'cache_tag' => 'responsecache',
'replacers' => [
\Spatie\ResponseCache\Replacers\CsrfTokenReplacer::class,
],Mọi tham số quan trọng đều đọc từ .env: bật tắt, thời gian sống, store, header debug. File .env.example nên để RESPONSE_CACHE_ENABLED=false để môi trường dev mặc định không cache, chỉ production mới bật. Khi nghi cache gây lỗi, có thể tắt Laravel ResponseCache bằng một dòng .env và php artisan config:cache mà không phải deploy lại code.
Cache profile: quyết định cái gì được cache
Profile mặc định của Laravel ResponseCache cache mọi request GET thành công. Với website có trang quản trị và người dùng đăng nhập, như thế là quá rộng. Một profile mẫu:
class SiteResponseCacheProfile extends BaseCacheProfile
{
public function shouldCacheRequest(Request $request): bool
{
if ($request->ajax()) return false;
if ($this->isRunningInConsole()) return false;
if (Auth::check()) return false; // người dùng đăng nhập
if (Auth::guard('admin')->check()) return false; // quản trị viên
return $request->isMethod('get');
}
public function shouldCacheResponse(Response $response): bool
{
if (! ($response->isSuccessful() || $response->isRedirection())) return false;
// chỉ cache HTML/text, không cache JSON
return Str::startsWith($response->headers->get('Content-Type', ''), 'text/');
}
public function useCacheNameSuffix(Request $request): string
{
// giao diện mobile và desktop khác nhau nên tách bản cache
return deviceType(); // 'mobile' hoặc 'desktop'
}
}Hậu tố useCacheNameSuffix() của Laravel ResponseCache cho phép cùng một URL có nhiều bản cache, cần thiết khi website render HTML khác nhau cho mobile và desktop. Giữ số biến thể càng ít càng tốt: mỗi hậu tố thêm vào là một bản cache nữa phải xoá khi nội dung đổi (xem mục 5).
Gắn middleware theo route (rút gọn)
Route::get('/', 'HomeController@index')->middleware('cache-response');
Route::get('/tag/{slug}', 'TagController@index')->middleware('cache-response');
Route::get('{slug}', 'HandleController@index') // bài viết, danh mục
->where('slug', '^(?!admin\/|ajax\/|sitemap\/)[a-zA-Z0-9-._\/]+$')
->middleware('cache-response');
Route::get('/lien-he', 'ContactController@index')->middleware('do-not-cache-response');
Route::post('/ajax/contact', 'AjaxController@sender')->middleware('do-not-cache-response');Chỉ trang công khai được bật Laravel ResponseCache. Trang có form, thông báo sau khi gửi và mọi endpoint POST được đánh dấu tường minh là không cache, để người đọc code sau hiểu đó là quyết định có chủ đích chứ không phải bỏ sót.
4. Xoá cache khi nội dung thay đổi
Cache chỉ có ích khi nó đúng. Biên tập viên sửa bài mà người đọc vẫn thấy bản cũ trong năm phút là lỗi nghiệp vụ. Mỗi lần tạo hoặc cập nhật bài viết, controller quản trị nên xoá cache truy vấn và bản Laravel ResponseCache của đúng URL đó, cho mọi hậu tố:
use Spatie\ResponseCache\Facades\ResponseCache;
// trong PostController@update, sau khi lưu database
Cache::store('redis')->forget('post_' . $id);
Cache::store('redis')->forget('post_slug_' . $slug);
foreach (['mobile', 'desktop'] as $suffix) { // mọi hậu tố profile có thể sinh
ResponseCache::selectCachedItems()
->usingSuffix($suffix)
->forUrls('/' . $slug)
->forget();
}Cùng cơ chế áp dụng cho bình luận: duyệt, trả lời, ẩn hay xoá bình luận đều xoá cache của bài chứa nó. Cache danh sách bình luận nên dùng remember(..., 60 * 30) thay vì rememberForever: nếu quên xoá ở đâu đó, dữ liệu cũ cũng tự hết hạn.
Cache truy vấn trên Redis
Laravel ResponseCache lo phần HTML, còn dữ liệu bên dưới vẫn cần cache riêng:
Cache::store('redis')->remember($type . '_' . $typeId, 60 * 30, function () use ($type, $typeId) {
return Comment::where('type', $type)->where('type_id', $typeId)->get();
});So với cache file, Redis cho phép nhiều tiến trình PHP-FPM đọc ghi cùng lúc mà không tranh chấp file trên đĩa, xoá một khoá có độ trễ thấp, và có thể đặt maxmemory để cache không ăn hết RAM của Cloud VPS. Cấu hình kết nối theo tài liệu Redis của Laravel 8, dùng database Redis riêng cho cache truy vấn và riêng cho Laravel ResponseCache.
5. Lỗi thường gặp khi dùng Laravel ResponseCache
Xoá cache chỉ đúng một biến thể
Lỗi phổ biến nhất: tính hậu tố từ request hiện tại khi xoá, tức request của biên tập viên đang bấm lưu. Biên tập viên dùng desktop thì chỉ bản "desktop" bị xoá, người đọc trên điện thoại vẫn thấy bài cũ tới khi cache hết hạn. Lỗi này khó phát hiện khi tự kiểm tra, vì người kiểm tra thường dùng đúng thiết bị vừa sửa bài. Luôn lặp qua mọi hậu tố như mục 4, hoặc dùng store hỗ trợ tag và xoá theo tag.
Cache nhầm trang có session, CSRF hoặc thông báo
Response chứa token CSRF hoặc thông báo "Gửi liên hệ thành công" mà bị cache thì người sau nhận token của người trước và form báo lỗi 419. CsrfTokenReplacer thay token bằng giá trị của phiên hiện tại mỗi lần trả cache. Thông báo flash và nội dung theo người dùng thì không có replacer nào lo hộ, phải loại trừ ở profile (Auth::check()) hoặc ở route (do-not-cache-response).
Xoá toàn bộ cache mà không biết
Khi không đặt cache_tag, lệnh php artisan responsecache:clear xoá toàn bộ cache store mà Laravel ResponseCache đang dùng. Nếu store đó cũng chứa cache truy vấn thì mọi thứ bị xoá cùng lúc, và lượt truy cập ngay sau đó dồn hết xuống MySQL. Hãy dùng store hoặc database Redis riêng cho Laravel ResponseCache.
Không biết request có trúng cache hay không
Bật tạm header thời điểm cache bằng biến riêng (RESPONSE_CACHE_DEBUG_HEADER) thay vì bật APP_DEBUG trên production, rồi so sánh hai lần curl -I liên tiếp.
6. Queue worker chạy bằng Supervisor
Một số việc sau khi lưu nội dung, như gọi API purge cache ở CDN, nên đẩy vào queue và chạy trễ vài giây (dispatch(...)->delay(3)) để request của biên tập viên không phải chờ. Muốn vậy cần queue:work chạy nền liên tục; Laravel khuyên dùng Supervisor theo hướng dẫn cấu hình Supervisor trong tài liệu Laravel 8. Như trong bài về mô hình Cloud VPS ba tầng, mỗi site có thể tự mang file supervisord.ini trong thư mục code:
[program:example.com-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/example.com/artisan queue:work --sleep=2 --tries=2 --timeout=120 --memory=500
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/example.com/storage/logs/worker.logBa lỗi hay gặp với worker:
- Đường dẫn còn trỏ về domain cũ: site dựng trên domain demo rồi mới chuyển sang domain chính, nhưng tên program và đường dẫn
artisantrongsupervisord.inivẫn là của bản demo. Worker vẫn "RUNNING" nhưng xử lý job bằng code của thư mục khác. Khi đổi domain, file supervisor phải nằm trong danh sách cần sửa. - Quên restart sau deploy: worker là tiến trình sống lâu, giữ code cũ trong bộ nhớ. Sau mỗi lần deploy cần
php artisan queue:restart, Supervisor sẽ tự dựng lại worker. QUEUE_CONNECTION=sync: giá trị mặc định trong.env.examplelàm job chạy ngay trong request,delay()bị bỏ qua. Production phải đặtredishoặcdatabase.
7. Checklist khi bật Laravel ResponseCache trên production
- Mặc định tắt, bật có chủ đích: chỉ gắn
cache-responsevào route công khai. - Profile loại trừ AJAX, người dùng đăng nhập, quản trị viên và response không phải HTML.
- Có
CsrfTokenReplacer; trang có form hoặc flash message đánh dấudo-not-cache-response. - Số hậu tố tối thiểu; code xoá cache lặp qua mọi hậu tố.
- Luôn có TTL: vài phút cho response, vài chục phút cho truy vấn.
- Laravel ResponseCache, cache truy vấn và queue dùng kết nối hoặc database Redis khác nhau; Redis có
maxmemory. - Worker chạy bằng Supervisor,
queue:restartsau mỗi lần deploy.
Laravel ResponseCache không thay được việc tối ưu query hay PHP-FPM, nhưng với website nội dung, nó là cách rẻ nhất để giảm số request thực sự phải chạm tới PHP và MySQL, với điều kiện đi kèm cơ chế xoá cache đúng và đủ.
Bài tiếp theo
Dựng lại toàn bộ Cloud VPS bằng cloud image và cloud-init. Máy mới được tạo từ cloud image ra sao, cloud-init làm những gì trong lần boot đầu, và cách xác định ngày dựng thật của một máy.
