+1

Redis Cache: Khi "Tấm Khiên" Trở Thành Tử Huyệt & 3 Thảm Họa Đẫm Máu

Ở những bài trước, chúng ta đã hiểu tại sao RAM lại nhanh gấp vạn lần ổ cứng, và cách tổ chức Key-Value sao cho khoa học. 99% anh em dev khi nhắc đến Redis đều nghĩ ngay đến việc dùng nó làm Cache (Bộ nhớ đệm) để che chắn cho Database.

Tư duy cơ bản là: "Có Cache thì web chạy nhanh, Database nhàn. Hết." Nhưng nếu bạn mang tư duy đó lên một hệ thống lớn (như sàn E-commerce lúc Flash Sale), tấm khiên Redis của bạn sẽ bị đâm thủng, và Database của bạn sẽ chết đứng chỉ trong vài giây.

Hôm nay, Vibe Coder sẽ mổ xẻ 3 thảm họa kinh điển nhất của hệ thống Caching và cách "bịt lỗ hổng" chuẩn Enterprise!

PHẦN 1: TUY DUY CACHE-ASIDE (MÔ HÌNH CHUẨN MỰC)

TRước khi bàn về thảm họa, chúng ta cần thống nhất Cache hoạt động. Mẫu thiết kế phổ biến nhất tên là Cache-Aside.

Luồng chạy của nó gồm 3 bước:

  1. App hỏi Redis: "Ê, có dữ liệu của bài viết ID=5 không?"
  2. Trường hợp Cache Hit (Trúng đích): Redis có dữ liệu -> Trả về ngay lập tức (Tốn 1ms). Database không hề biết gì.
  3. Trường hợp Cache Miss (Trượt): Redis không có dữ liệu (hoặc đã hết hạn TTL) -> App phải lặn lội xuống MySQL để lấy data (Tốn 50ms) -> Trả về cho User, ĐỒNG THỜI copy một bản nhét ngược lại vào Redis để lần sau xài.

Mọi thứ nghe rất hoàn hảo. Vậy Database sập bằng cách nào?

PHẦN 2: 3 KỴ SĨ KHẢI HUYỀN CỦA HỆ THỐNG CACHE

Thảm họa 1: Cache Penetration (Xuyên Thủng Cache) Kịch bản: Hệ thống của bạn chỉ có các bài viết mang ID từ 1 đến 10.000. Một gã Hacker ác ý viết tool spam gọi API của bạn liên tục với ID là -1, hoặc 999999999 (những ID không hề tồn tại).

  • Kết quả: Vì ID không tồn tại, Redis trả về Cache Miss. App chạy xuống MySQL tìm. MySQL cũng không thấy, trả về NULL. Vì là NULL, App KHÔNG LƯU vào Redis.
  • Lần tiếp theo Hacker gọi ID=-1, App lại tiếp tục chạy xuống MySQL! Hàng vạn request xuyên thẳng qua Redis đập nát MySQL. Tấm khiên của bạn trở nên vô dụng!

Giải pháp Vibe Coder:

  1. Cache luôn cả giá trị rỗng (Cache Null): Nếu MySQL trả về NULL, hãy lấy luôn cái chữ NULL đó lưu vào Redis với TTL ngắn (VD: 30 giây). Lần sau Hacker gọi, Redis sẽ tự tin trả về NULL mà không cần làm phiền MySQL.
  2. Bloom Filter (Lưới lọc ma thuật): Dùng cấu trúc dữ liệu đặc biệt này để kiểm tra xem ID có tồn tại trong hệ thống hay không TRƯỚC KHI gọi vào Redis/MySQL.

Thảm họa 2: Cache Breakdown (Đánh Thủng Cache - Điểm Mù Hot Key) Kịch bản: Bài báo về vụ xì-căng của một Celeb vừa được đăng. Đây là một Hot Key (Dữ liệu cực nóng). Đang có 100.000 user F5 đọc bài báo đó mỗi giây. Tải đang dồn hết vào Redis, MySQL vẫn bình yên. Nhưng... Đùng một cái, đúng lúc đó Key trên Redis hết hạn (TTL = 0).

  • Kết quả: Ngay tại mili-giây mà Key bốc hơi, 100.000 user cùng nhận được Cache Miss. Cả 100.000 luồng code (threads) của App CÙNG LÚC ùa xuống MySQL để chạy câu lệnh SELECT bài báo đó hòng cập nhật lại Cache. MySQL đột quỵ ngay lập tức vì quá tải kết nối!

Giải pháp Vibe Coder: Mutex Lock (Khóa phân tán) Không cho phép đám đông chạy ùa xuống DB. Khi Cache Miss xảy ra, thằng request đầu tiên sẽ phải cầm một cái "Chìa khóa" (Lock). Chỉ thằng có khóa mới được phép chạy xuống MySQL lấy data. 99.999 thằng còn lại đứng đó đợi 50ms, sau đó quay lại Redis lấy data (lúc này thằng đầu tiên đã update Cache xong rồi).

// Code minh họa logic Mutex Lock trong Node.js
async function getArticle(id) {
    let article = await redis.get(`article:${id}`);
    if (article) return JSON.parse(article);

    // Cache Miss! Dùng lệnh setnx để tranh giành cái khóa
    const lock = await redis.setnx(`lock:article:${id}`, "locked", "EX", 5);
    
    if (lock) {
        // Cầm được khóa! Chạy xuống DB
        article = await db.query(`SELECT * FROM articles WHERE id = ?`, [id]);
        await redis.set(`article:${id}`, JSON.stringify(article), "EX", 3600);
        await redis.del(`lock:article:${id}`); // Lấy xong thì vứt khóa đi
        return article;
    } else {
        // Mất lượt lấy khóa. Đợi 50ms rồi thử đọc lại từ Cache
        await sleep(50);
        return getArticle(id); // Đệ quy gọi lại chính mình
    }
}

Thảm họa 3: Cache Avalanche (Tuyết Lở Cache) Kịch bản: Lúc nửa đêm, bạn chạy một Job đồng bộ 10.000 sản phẩm lên Redis và set cho TẤT CẢ bọn chúng chung một hạn sử dụng là TTL = 3600 (1 tiếng). Đúng 1 tiếng sau, toàn bộ 10.000 Key đó đồng loạt hết hạn và biến mất khỏi RAM cùng một tích tắc.

  • Kết quả: Ngay khoảnh khắc đó, bất kỳ user nào truy cập vào trang chủ cũng sẽ bị Cache Miss. MySQL phải hứng trọn hàng vạn request lấy dữ liệu mới cho 10.000 sản phẩm kia. Hệ thống sụp đổ như một trận tuyết lở.

Giải pháp Vibe Coder: Thêm sự ngẫu nhiên (TTL Jitter) Quy tắc vàng: Không bao giờ set TTL giống hệt nhau cho một lượng lớn Key được tạo cùng lúc. Hãy cộng thêm một số giây ngẫu nhiên vào TTL. Thay vì set cứng 3600 giây, hãy set: TTL = 3600 + random(0, 300). Lúc này, các Key sẽ hết hạn "lác đác" kéo dài trong khoảng 5 phút. Database sẽ chỉ nhận vài request rải rác để update Cache, không bao giờ bị dội bom.

Lời kết

Việc lưu dữ liệu vào Redis thì rất dễ. Nhưng việc thiết kế một hệ thống Cache có khả năng "tự bảo vệ" trước những đợt sóng traffic bất thường, Hacker đục khoét hay những hiệu ứng vật lý ngầm như Tuyết lở mới là đỉnh cao của System Design. Đừng để tấm khiên đắt tiền của bạn lại chính là mũi giáo đâm chết Database nhé!

Chủ đề tiếp theo: Khi Dữ Liệu Rơi Rớt - Giao Dịch (Transactions) và Bóng Ma Deadlock Trong SQL

Chúng ta đã bao bọc Database bằng Cache. Nhưng sâu thẳm bên trong cái Database đó, khi nó trực tiếp ghi dữ liệu, nó cũng phải đối mặt với những thảm họa của riêng mình.

Giả sử hệ thống Ngân hàng: Bạn chuyển 1 triệu cho người khác. Lệnh 1: Trừ 1 triệu ở ví của bạn. Lệnh 2: Cộng 1 triệu vào ví người kia. Nếu lệnh 1 chạy xong, server mất điện cái rụp, lệnh 2 không kịp chạy! Tiền của bạn đã bốc hơi khỏi vũ trụ!

Làm sao để SQL đảm bảo: "Hoặc là tao thành công cả 2, hoặc là tao hủy hết làm lại từ đầu"? Ở bài viết tới, chúng ta sẽ bước vào thế giới căng não của ACID, Database Transactions, và lỗi đáng sợ nhất của lập trình viên Backend: Deadlock (Khóa Chết). Anh em sẵn sàng nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí