Series Mô hình Hot/Cold trong Redis #2: Tuyệt kỹ Đồng bộ (Write Patterns) và Giải phóng Bộ nhớ (LRU)
1. Chiến lược Write-Through (Ghi xuyên thấu): Chắc cốp tuyệt đối
Để giải quyết tình trạng Stale Data khi nạp tiền, chúng ta không được phép chỉ ghi vào Database. Ứng dụng phải chịu trách nhiệm cập nhật cùng lúc cả dữ liệu Lạnh (PostgreSQL) và dữ liệu Nóng (Redis).
Luồng hoạt động (Khi hành khách nạp tiền tại TVM):
- Ứng dụng cập nhật số dư vào PostgreSQL.
- Ngay lập tức, ứng dụng ép Redis phải cập nhật lại (hoặc xóa đi) Key chứa số dư cũ.
Triển khai trong Laravel:
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use App\Models\Wallet;
class TopUpService
{
public function addMoney($cardUid, $amount)
{
DB::transaction(function () use ($cardUid, $amount) {
// 1. Ghi vào Cold Data (PostgreSQL)
$wallet = Wallet::where('card_uid', $cardUid)->lockForUpdate()->first();
$wallet->balance += $amount;
$wallet->save();
// 2. Ghi xuyên thấu lên Hot Data (Redis)
$cacheKey = "wallet_balance:{$cardUid}";
// Xóa cache cũ đi để lần quẹt thẻ tới, hệ thống (Cache-Aside) tự động kéo cục dữ liệu mới lên
Cache::forget($cacheKey);
// Hoặc có thể chủ động đè dữ liệu mới lên luôn:
// Cache::put($cacheKey, $wallet->balance, 86400);
});
}
}
Ưu điểm: Dữ liệu nhất quán 100%. Hành khách nạp tiền xong quẹt thẻ được ngay. Nhược điểm: Tốc độ Ghi (Write) bị chậm lại một chút vì phải đợi cả Database và Cache phản hồi.
2. Chiến lược Write-Behind / Write-Back (Ghi lùi): Vũ khí của hệ thống Tỷ Request
Mô hình Write-Through rất tốt cho việc nạp tiền (ít xảy ra). Nhưng khi hành khách quẹt thẻ trừ tiền (Tap Out) vào giờ cao điểm, nếu 10.000 người quẹt cùng lúc mà ta bắt hệ thống phải kết nối vào PostgreSQL để UPDATE từng dòng thì Database sẽ sập ngay lập tức (Nút thắt cổ chai Disk I/O).
Ở đẳng cấp Senior, chúng ta đảo ngược tư duy với Write-Behind: Chúng ta coi Redis là "Sự thật duy nhất" (Source of Truth) tại thời điểm quẹt thẻ. Ứng dụng chỉ trừ tiền trên Redis (Hot Data) rồi báo mở cổng ngay lập tức. Việc ghi xuống PostgreSQL (Cold Data) sẽ được giao cho một "công nhân" chạy ngầm làm sau.
Triển khai thực chiến kết hợp Message Queue (Beanstalkd/Kafka):
class TapOutService
{
public function processTapOut($cardUid, $fare)
{
$cacheKey = "wallet_balance:{$cardUid}";
// 1. Lệnh DECRBY của Redis thao tác trực tiếp trên RAM, tốc độ dưới 1ms
$newBalance = Redis::decrby($cacheKey, $fare);
if ($newBalance < 0) {
// Rollback lại số dư nếu âm tiền
Redis::incrby($cacheKey, $fare);
throw new Exception("Không đủ tiền!");
}
// 2. Mở cổng ngay lập tức!
$this->openGate();
// 3. Ném một tờ giấy nhắn (Job) vào Hàng đợi (Queue)
// Báo cho các Worker biết: "Ê, lúc nào rảnh nhớ trừ tiền cái thẻ này dưới Database nhé!"
SyncWalletToDatabaseJob::dispatch($cardUid, $fare, now())->onQueue('hitachi_sync');
}
}
Với thiết kế này, Database của bạn hoàn toàn "bất tử" trước mọi cơn bão traffic, vì nó không còn phải đối mặt trực tiếp với luồng sự kiện quẹt thẻ nữa!
3. Safety Net: Thuật toán trục xuất LRU (Least Recently Used)
Dù bạn có set TTL 24h (như Bài 1), nhưng giả sử hôm nay là ngày lễ, lượng khách đột biến lên 5 triệu người. RAM của server Redis bị lấp đầy 100% khi thời hạn 24h còn chưa kịp kết thúc. Lúc này Redis sẽ báo lỗi OOM command not allowed.
Thay vì để hệ thống chết, chúng ta phải nói cho Redis biết: "Nếu hết RAM, hãy tự động tìm những dữ liệu nào lâu rồi không ai đụng tới và đá nó ra khỏi bộ nhớ".
Đó chính là cấu hình Eviction Policy trong file redis.conf:
# Giới hạn RAM tối đa cho Redis (Ví dụ 4GB)
maxmemory 4gb
# Chính sách trục xuất dữ liệu khi đầy RAM
maxmemory-policy allkeys-lru
- allkeys-lru: Đây là cấu hình "chân ái". Khi đầy RAM, Redis sẽ theo dõi và tìm ra những Key bị "bỏ quên" lâu nhất (Least Recently Used) để xóa đi, nhường chỗ cho dữ liệu mới. Bằng cách này, tập dữ liệu trên Redis của bạn luôn luôn là những Hot Data tinh khiết nhất!
Tổng kết Bài 2
Chúng ta đã hoàn thiện vòng tròn thao tác trên mô hình Hot/Cold:
- Đọc nhanh: Dùng Cache-Aside để kéo dữ liệu lạnh lên làm nóng.
- Ghi an toàn (Nạp tiền): Dùng Write-Through để đảm bảo dữ liệu nhất quán ngay lập tức.
- Ghi siêu tốc (Trừ tiền): Dùng Write-Behind kết hợp Hàng đợi ngầm (Background Job) để đưa tốc độ phản hồi về sát mức 0ms.
- Chống tràn RAM: Dùng chính sách allkeys-lru để hệ thống tự duy trì hơi thở.
Hệ thống của bạn bây giờ gần như không thể bị đánh sập bởi yếu tố I/O (Đọc/Ghi) thông thường.
Tuy nhiên, vẫn còn một tử huyệt cuối cùng mang tên Cache Stampede (Bão Cache / Hiệu ứng bầy đàn): Điều gì xảy ra khi một tập Hot Data quan trọng (ví dụ: File cấu hình giá vé Metro) vừa hết hạn TTL, và ngay lúc đó có 10.000 request ập tới đòi đọc nó? (Lúc này 10.000 request sẽ cùng chọc thủng Redis và đánh thẳng vào Database cùng 1 giây!).
All rights reserved