Series Docker & Redis cho Laravel #2: Nghệ thuật Caching và Chống "Dội bom" Database
Ở Bài 1, chúng ta đã đúc xong một "Lõi nhớ siêu tốc" bằng Redis an toàn, bền bỉ trên Docker và cấu hình Laravel để tách biệt các luồng dữ liệu (Cache, Session, Queue). Hạ tầng đã vững chắc, giờ là lúc chúng ta mang nó ra chiến trường.
Khi làm việc với những hệ thống lưu lượng cao (High-Traffic) như mạng lưới AFC Metro, việc dùng Cache không chỉ đơn giản là Cache::put() và Cache::get(). Nếu không cẩn thận, chính cái Cache đó sẽ quay lại "giết" chết Database của bạn. Hôm nay, chúng ta sẽ đi sâu vào nghệ thuật Caching chuẩn Senior: Giải quyết bài toán Race Condition, Cache Stampede và quản lý vùng nhớ thông minh.
1. Phân vùng Cache thông minh với Cache Tags
Vấn đề: Giả sử bạn dùng Redis để cache thông tin cấu hình của tất cả các nhà ga. Khi nhà ga Ba Son (S03) thay đổi cấu hình giá vé, bạn cần xóa cache để hệ thống cập nhật. Nếu bạn dùng lệnh Cache::flush(), bạn sẽ vô tình xóa luôn cache của toàn bộ 20 nhà ga khác, khiến Database đột ngột phải gánh một lượng tải khổng lồ để nạp lại từ đầu.
Thuốc giải: Cache Tags Lợi thế tuyệt đối của Redis (so với file hay database cache) là nó hỗ trợ Tags. Bạn có thể "gắn nhãn" cho các key để gộp chúng lại thành từng nhóm.
use Illuminate\Support\Facades\Cache;
public function getStationConfig($stationId)
{
// Gắn 2 nhãn: 'stations' (nhãn chung) và 'station_S03' (nhãn riêng)
return Cache::tags(['stations', 'station_' . $stationId])->remember('config_' . $stationId, 3600, function () use ($stationId) {
return StationConfig::where('station_id', $stationId)->first();
});
}
Bây giờ, khi ga Ba Son (S03) cập nhật, bạn chỉ việc "bắn tỉa" chính xác cái nhãn đó:
// Chỉ xóa sạch những key nào có gắn nhãn 'station_S03'
// Cấu hình của ga Bến Thành (S01) vẫn sống khỏe trong RAM!
Cache::tags('station_S03')->flush();
2. Cơn ác mộng "Cache Stampede" (Hiệu ứng bầy đàn)
Đây là câu hỏi phỏng vấn kinh điển cho vị trí Senior Backend. Kịch bản: Thông tin vé tháng (Monthly Pass) của khách hàng được cache trong 1 giờ. Hết 1 giờ, key này hết hạn (expire) và biến mất khỏi Redis. Đúng lúc đó, vào giờ cao điểm, có 500 request quẹt thẻ của hành khách đó dội vào Server cùng một mili-giây.
Vì cache đã mất, cả 500 request này đều đi xuyên qua Redis, lao thẳng vào Database (MySQL) để chạy câu lệnh SELECT. MySQL bị quá tải (CPU 100%) và sập ngay lập tức. Đây gọi là hiện tượng Cache Stampede.
Thuốc giải: Atomic Locks (Khóa nguyên tử) Chúng ta sẽ dùng Redis để tạo ra một cái "Khóa". Khi cache hết hạn, trong 500 request lao tới, chỉ có request ĐẦU TIÊN vớ được cái khóa này và được phép đi vào Database. 499 request còn lại sẽ phải đứng ngoài cửa đợi request đầu tiên lấy data về rồi nhét lại vào Cache.
Laravel hỗ trợ Redis Lock cực kỳ thanh lịch thông qua hàm Cache::lock():
use Illuminate\Support\Facades\Cache;
public function getMonthlyPassInfo($userId)
{
$cacheKey = 'monthly_pass_' . $userId;
// Lấy dữ liệu từ cache
$data = Cache::get($cacheKey);
// Nếu cache miss (hết hạn hoặc chưa có)
if (is_null($data)) {
// Tạo một cái khóa trong 5 giây cho riêng user này
$lock = Cache::lock('lock_monthly_pass_' . $userId, 5);
// Block(5): Bắt các request đi sau phải đứng đợi tối đa 5 giây để lấy khóa
if ($lock->block(5)) {
try {
// Đề phòng trường hợp request trước đó đã lấy data và nhét vào cache rồi
// Nên lấy lại một lần nữa cho chắc ăn (Double-check)
$data = Cache::get($cacheKey);
if (is_null($data)) {
// Chỉ 1 request duy nhất được chạy dòng SQL này
$data = Pass::where('user_id', $userId)->first();
Cache::put($cacheKey, $data, 3600);
}
} finally {
// Xử lý xong phải trả lại khóa ngay lập tức
$lock->release();
}
}
}
return $data;
}
Nhờ cơ chế này, Database của bạn đã được bọc một lớp áo giáp chống lại mọi đợt DDoS nội bộ từ chính các luồng xử lý đồng thời!
3. Đếm số siêu tốc chống Race Condition (Atomic Increment)
Trong hệ thống AFC, bạn cần đếm tổng số lượt khách đi qua một cổng Turnstile (máy quẹt vé) trong ngày. Nếu bạn dùng logic thông thường: Đọc giá trị hiện tại -> Cộng thêm 1 -> Lưu lại.
// ❌ CỰC KỲ NGUY HIỂM Ở MÔI TRƯỜNG CHỊU TẢI CAO (Race Condition)
$count = Cache::get('turnstile_A_count'); // Lấy ra số 10
$count = $count + 1; // Thành 11
Cache::put('turnstile_A_count', $count); // Ghi lại 11
Nếu có 2 người quẹt thẻ cùng lúc, cả 2 luồng (thread) cùng lấy ra số 10, cùng cộng thành 11 và cùng ghi đè lên nhau. Kết quả: 2 người qua trạm nhưng hệ thống chỉ đếm là 11 (mất mát dữ liệu).
Thuốc giải: Lệnh increment của Redis Redis chạy đơn luồng (Single-threaded) với các lệnh Atomic. Nghĩa là lệnh cộng 1 của nó xảy ra ở mức độ nguyên tử, không một luồng nào có thể chen ngang.\
use Illuminate\Support\Facades\Redis;
public function logTapIn($gateId)
{
// ✅ Chuẩn Senior: Giao phó việc cộng số cho Redis xử lý ở tầng lõi C-extension
// Vừa nhanh gấp hàng trăm lần, vừa không bao giờ bị đè dữ liệu
Redis::incr('turnstile_' . $gateId . '_count');
}
Bất cứ khi nào bạn cần đếm (lượt view, lượt giao dịch, đếm số lần sai mật khẩu để block IP), hãy sử dụng lệnh incr() hoặc decr() trực tiếp của Redis thay vì tự tính toán bằng PHP.
Bằng việc áp dụng Cache Tags để dọn dẹp chính xác, Atomic Locks để chống Cache Stampede và Atomic Increment để đếm dữ liệu song song, hệ thống API của bạn giờ đây đã có khả năng đứng vững trước những đợt tăng tải đột ngột (Spike Traffic) ở mọi khung giờ cao điểm!
All Rights Reserved