+1

Mổ Xẻ Đoạn Code Caching "Quốc Dân": Câu Hỏi Phỏng Vấn Tưởng Dễ Nhưng Lại Lộ Vết Sẹo "Thủng Cache" Chết Người

Chào anh em Viblo! 👋

Lại là một câu hỏi phỏng vấn "đậm mùi hành tỏi" từ các vị Mentor/Senior. Đoạn code

static async getCachedData(key, model, query = {}, ttl = 3600, attributes = null) {

    try {

        const cachedData = await redis.get(key);

        if (cachedData) {

            return JSON.parse(cachedData);

        } else {

            const data = await model.findAll({

                ...query,

                attributes: attributes || undefined,

                raw: true,

            });



            if (data && data.length > 0) {

                await redis.set(key, JSON.stringify(data), 'EX', ttl);

            }

            return data;

        }

    } catch (error) { ... }

}

CacheHelper.getCachedData ở trên cực kỳ quen thuộc, anh em lướt qua các project Node.js sử dụng Sequelize/Mongoose thì phải có tới 80% hệ thống viết theo kiểu này để tối ưu hóa dữ liệu ít thay đổi (Master Data) như Tỉnh/Thành, Quận/Huyện, Cấu hình hệ thống...

Nhìn thì có vẻ rất clear và mượt mà, nhưng bằng con mắt của một người thức hôm thức khuya cứu sập cả hệ thống, đoạn code này ẩn chứa một cái bẫy bảo mật và hiệu năng cực kỳ lớn

Hãy cùng mình lần lượt trả lời 3 câu hỏi của "Thầy" để bóc tách xem đoạn code này "ngoan" hay "hư" nhé!

1. Đây là kỹ thuật/pattern caching gì? Mô tả luồng hoạt động

Đoạn code trên áp dụng chính xác thiết kế Cache-Aside Pattern (hay còn gọi là Lazy Loading) Đây là cơ chế caching phổ biến nhất ở tầng Application, nơi mà ứng dụng của bạn sẽ chủ động đứng ra điều phối dữ liệu giữa Cache (Redis) và Database.

Luồng hoạt động này gồm 4 bước:

  1. Bước 1 (Check Cache): Khi có request gọi hàm, nó sẽ ngó vào Redis trước để tìm dữ liệu theo key.
  2. Bước 2 (Cache Hit): Nếu thấy dữ liệu trong Redis (cachedData truthy), nó lập tức JSON.parse và trả về luôn cho Controller. DB hoàn toàn thành thơi.
  3. Bước 3 (Cache Miss): Nếu Redis báo trống (không có key hoặc key đã hết hạn), ứng dụng sẽ mò xuống database thông qua hàm model.findAll(query).
  4. Bước 4 (Nạp Cache): Sau khi lấy được dữ liệu từ DB, nó sẽ tranh thủ nhét dữ liệu này vào Redis kèm thời gian hết hạn (ttl) để phục vụ cho những request đến sau, rồi mới trả dữ liệu về cho người dùng

2. Tại sao họ lại bọc nó thành một hàm dùng chung (Generic Helper)?

Chính cái ý tốt "tiết kiệm RAM" này lại vô tình mở toang cánh cửa cho một cuộc tấn công từ chối dịch vụ (DoS/DDoS) hoặc tự làm sập mình khi có high traffic.

Tưởng tượng kẻ xấu (hoặc một con bug ở Frontend) liên tục gửi request lấy dữ liệu với một ID không bao giờ tồn tại trong hệ thống (ví dụ: district_id = -999999).

  1. Request vào hệ thống -> Vào Redis tìm key district:-999999 -> Cache Miss (Vì không có). 2. Hệ thống mò xuống DB query: SELECT * FROM districts WHERE id = -999999 -> Kết quả trả về mảng rỗng [] (data.length = 0). 3. Gặp dòng IF thần thánh: Vì data.length = 0, hàm quyết định KHÔNG SET CACHE vào Redis. 4. Kết quả trả về cho client là []

Chuyện gì xảy ra tiếp theo? Request thứ 2, thứ 3... thứ 1 triệu của cái ID -999999 đó tràn vào. Vì Redis không bao giờ lưu cái kết quả trống đó, nên cả 1 triệu request này đều vượt mặt qua Redis và đấm thẳng xuống Database! Database sẽ phải căng mình ra chạy câu query tìm kiếm vô nghĩa đó liên tục, CPU tăng vọt lên 100% và sập nguồn. Đó chính là định nghĩa của Cache Penetration.

Giải pháp khắc phục (Bốc thuốc chuẩn Senior)

Đừng phân biệt đối xử với dữ liệu rỗng. Nếu DB bảo không có, hãy tin DB và cache luôn cả cái kết quả rỗng đó để bảo vệ DB.

// ❌ Sửa dòng code lỗi thời:
// if (data && data.length > 0) { ... }

// ✅ Thay bằng luồng an toàn:
if (data) {
    // Nếu dữ liệu rỗng, ta vẫn lưu vào Redis là '[]'
    // Nhưng có thể tinh tế đặt một cái TTL ngắn hơn (ví dụ: 5 phút thay vì 1 tiếng)
    const customTtl = data.length === 0 ? 300 : ttl; 
    await redis.set(key, JSON.stringify(data), 'EX', customTtl);
}

Bằng cách này, khi kẻ xấu spam ID lỗi -999999 lần thứ hai, Redis sẽ lập tức trả về mảng rỗng [] trong vòng vài mili-giây, chặn đứng request ngay tại "vòng gửi xe" và cứu sống Database bên dưới.

Hy vọng phần mổ xẻ câu hỏi phỏng vấn này mang lại cho anh em một góc nhìn thực chiến và sâu sắc hơn về Caching. Đôi khi chỉ một dòng IF tưởng như tối ưu lại là nguồn cơn của những pha sập hệ thống đi vào lòng đất.

Chúc anh em tự tin vượt qua các vòng phỏng vấn tiếp theo! Happy Coding! 🚀💻


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í