Tại sao chiến lược API của bạn cần Caching
Một yêu cầu API đơn lẻ thường kích hoạt một chuỗi các sự kiện nặng nề. Máy chủ của bạn có thể mất 200ms để truy vấn cơ sở dữ liệu, xử lý logic nghiệp vụ và tuần tự hóa JSON. Nếu dữ liệu không thay đổi kể từ lần yêu cầu cuối cùng, nỗ lực đó sẽ bị lãng phí. Tôi đã từng thấy những tinh chỉnh header đơn giản giúp các công ty tiết kiệm hàng ngàn đô la chi phí truyền tải dữ liệu đám mây hàng tháng bằng cách ngăn chặn việc truyền dữ liệu dư thừa.
Caching về cơ bản là việc tránh những công việc không cần thiết. Bằng cách cung cấp một bản sao đã lưu trữ của tài nguyên, bạn có thể giảm đáng kể tải cho máy chủ đối với các ứng dụng có nhiều lượt đọc. Đây không chỉ là một sự thúc đẩy hiệu suất. Nó cung cấp một “lưới an toàn” quan trọng giúp hạ tầng của bạn đứng vững trong những đợt lưu lượng truy cập tăng đột biến không mong muốn.
Các trụ cột cốt lõi: Freshness và Validation
Bạn không cần Redis hay một hạ tầng phức tạp để bắt đầu caching. Giao thức HTTP đã tích hợp sẵn các tính năng này. Để triển khai đúng cách, bạn chỉ cần nắm vững hai khái niệm: Freshness (Độ tươi mới) và Validation (Xác thực).
Các header Freshness cho client biết tài nguyên sẽ còn hiệu lực trong bao lâu. Hãy coi header Cache-Control như một ngày hết hạn. Các header Validation, như ETag, đóng vai trò như một checksum. Chúng cho phép client hỏi: “Tôi đang có phiên bản X của dữ liệu này; tôi có thực sự cần tải lại nó không?”
Trong các môi trường hiện đại như Node.js hoặc Go, bạn hiếm khi phải “cài đặt” caching. Thay vào đó, bạn cấu hình middleware để chèn các header này vào phản hồi. Dưới đây là một endpoint Express tiêu chuẩn trước khi chúng ta thêm bất kỳ tối ưu hóa nào:
const express = require('express');
const app = express();
app.get('/api/products', (req, res) => {
const products = [{ id: 1, name: 'Laptop' }, { id: 2, name: 'Phone' }];
res.json(products);
});
app.listen(3000, () => console.log('Máy chủ đang chạy trên cổng 3000'));
Chọn chính sách Cache-Control phù hợp
Header Cache-Control là đòn bẩy chính cho hiệu suất của bạn. Nó là một chỉ thị đa thành phần dành cho trình duyệt và CDN. Cấu hình sai cái này là nguồn cơn phổ biến của các lỗi “dữ liệu cũ” (stale data), vì vậy sự chính xác là rất quan trọng.
Public so với Private
Dữ liệu chung, chẳng hạn như danh sách 50 bài đăng blog công khai, nên được đánh dấu là public. Điều này cho phép các CDN lưu trữ phản hồi và phục vụ cho hàng ngàn người dùng khác. Ngược lại, dữ liệu riêng biệt của người dùng như trang quản trị thanh toán phải là private. Điều này đảm bảo chỉ trình duyệt của người dùng đó mới lưu trữ dữ liệu, giữ cho thông tin nhạy cảm không nằm trên các máy chủ trung gian dùng chung.
Thiết lập max-age
Chỉ thị max-age xác định Thời gian sống (TTL) tính bằng giây. Đối với danh mục sản phẩm cập nhật mỗi giờ một lần, hãy sử dụng 3600. Đối với các tài sản tĩnh không bao giờ thay đổi, bạn có thể đặt lên tới một năm (31536000):
Cache-Control: public, max-age=3600
Sự khác biệt giữa No-Cache và No-Store
- no-cache: Đây là một cái tên hơi gây hiểu lầm. Nó nói với trình duyệt rằng nó có thể lưu trữ dữ liệu, nhưng *phải* kiểm tra với máy chủ trước khi sử dụng. Điều này hoàn hảo khi bạn muốn tốc độ của ETags mà không gặp rủi ro hiển thị thông tin cũ.
- no-store: Sử dụng cái này cho các endpoint bảo mật cao, như token đặt lại mật khẩu. Nó cấm trình duyệt và tất cả các proxy lưu lại bất kỳ bản sao nào của dữ liệu.
Tiết kiệm băng thông với ETags
ETags (Entity Tags) xử lý khía cạnh xác thực của bài toán. Một ETag là một chuỗi duy nhất—thường là mã hash MD5—đại diện cho trạng thái hiện tại của tài nguyên. Khi dữ liệu thay đổi, mã hash sẽ thay đổi.
Quy trình làm việc rất đơn giản. Đầu tiên, máy chủ gửi một phản hồi kèm theo ETag: "v123". Ở yêu cầu tiếp theo, client gửi lại giá trị đó trong header If-None-Match: "v123". Nếu dữ liệu giống hệt nhau, máy chủ sẽ trả về trạng thái 304 Not Modified. Thân phản hồi (body) sẽ trống, giúp tiết kiệm một lượng lớn băng thông đối với các gói dữ liệu JSON lớn.
const crypto = require('crypto');
app.get('/api/data', (req, res) => {
const data = { message: "Xin chào Thế giới", timestamp: "2023-10-27" };
const jsonString = JSON.stringify(data);
const etag = crypto.createHash('md5').update(jsonString).digest('hex');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, no-cache');
res.send(data);
});
Mặc dù các framework thường tự động hóa việc này, việc triển khai thủ công giúp bạn phát hiện ra các lỗi tiềm ẩn. Ví dụ, nếu JSON của bạn bao gồm một timestamp “generated_at”, ETag của bạn sẽ thay đổi mọi lúc, khiến cache trở nên vô dụng.
Xác minh triển khai của bạn
Đừng bao giờ mặc định rằng cache của bạn đang hoạt động chỉ vì bạn đã viết code. Hãy mở tab Network của trình duyệt và kiểm tra cột “Size”. Bạn sẽ muốn thấy dòng “(from disk cache)” hoặc trạng thái “304”.
Kiểm tra với cURL
Tôi thích dùng curl để xác minh nhanh các header. Sử dụng flag -I để chỉ xem các header mà không bị rối bởi phần thân:
curl -I http://localhost:3000/api/data
Để mô phỏng một người dùng quay lại, hãy gửi lại ETag cho máy chủ:
curl -I -H 'If-None-Match: "ma-hash-cua-ban-o-day"' http://localhost:3000/api/data
Một thiết lập thành công sẽ trả về HTTP/1.1 304 Not Modified.
Cẩn thận với header Vary
Một sai lầm phổ biến là bỏ qua header Vary. Nếu API của bạn phục vụ các nội dung khác nhau dựa trên header Authorization hoặc Accept-Language, bạn phải báo cho cache biết. Nếu không, Người dùng A có thể vô tình nhìn thấy phiên bản cache dữ liệu của Người dùng B.
Vary: Authorization, Accept-Encoding
Nắm vững các header này sẽ biến một API mong manh thành một hệ thống mạnh mẽ, có khả năng mở rộng. Đây là một tối ưu hóa có tác động lớn, yêu cầu lượng code tối thiểu nhưng mang lại hiệu quả vượt trội khi lượng người dùng của bạn tăng lên.

