0
0
Lập trình
TT

Tối Ưu Hiệu Suất Node.js API với Redis Caching

Đăng vào 6 tháng trước

• 5 phút đọc

Giới Thiệu

Nếu bạn là một kỹ sư full-stack chịu trách nhiệm cho một API Node.js có lưu lượng truy cập cao, chắc chắn bạn đã cảm thấy sự khó chịu của những đợt trễ. Tin tốt là một lớp cache được lập kế hoạch tốt có thể giảm thiểu thời gian phản hồi, giảm tải cho cơ sở dữ liệu và cải thiện trải nghiệm người dùng tổng thể. Hướng dẫn thực tế này sẽ đưa bạn qua một quy trình tối ưu hóa hiệu suất từng bước, sử dụng Redis, Docker và một vài mẫu bất đồng bộ có thể áp dụng vào bất kỳ mã nguồn nào.

1. Xác Định Các Đường Dẫn Nóng

Trước khi thêm bất kỳ bộ nhớ đệm nào, bạn cần biết cái gì cần được lưu trữ.

  • Xác định các endpoint có tần suất cao (ví dụ: /products, /user/profile).
  • Đo lường thời gian phản hồi trung bình bằng cách sử dụng lệnh curl hoặc công cụ như hey.
  • Kiểm tra chi phí truy vấn cơ sở dữ liệu bằng cách sử dụng EXPLAIN trong Postgres hoặc SHOW PROFILE trong MySQL.
Copy
hey -n 10000 -c 50 https://api.example.com/products

Nếu độ trễ trung vị trên 200 ms và truy vấn cơ sở dữ liệu cho thấy quét toàn bộ bảng, bạn đã tìm thấy một ứng viên cần tối ưu.

2. Chọn Chiến Lược Caching Phù Hợp

Chiến Lược Thời Điểm Sử Dụng Ưu Điểm Nhược Điểm
Cache-Aside (Lazy) Đọc nhiều, ghi ít Đơn giản, không có dữ liệu cũ khi ghi Yêu cầu truy vấn cơ sở dữ liệu cho yêu cầu đầu tiên
Write-Through Cập nhật thường xuyên, tính nhất quán nghiêm ngặt Cơ sở dữ liệu luôn được cập nhật Độ trễ ghi cao hơn một chút
Time-Based TTL Dữ liệu thay đổi theo lịch Tự động hết hạn Có thể phục vụ dữ liệu cũ cho đến khi TTL hết hạn
Cache Invalidation via Events Cần cập nhật theo thời gian thực Gần như không có độ cũ Cần hạ tầng pub/sub bổ sung

Đối với hầu hết các API công khai, cache-aside với TTL ngắn là điểm ngọt. Bạn giữ cho việc triển khai nhẹ nhàng trong khi vẫn có được những cải thiện đáng kể về tốc độ đọc.

3. Kết Nối Redis Trong Dịch Vụ Node.js Của Bạn

3.1 Thêm thư viện client

Copy
npm install ioredis@5

3.2 Tạo một lớp bao bọc Redis có thể tái sử dụng

javascript Copy
// redisClient.js
const Redis = require('ioredis');
const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: Number(process.env.REDIS_PORT) || 6379,
  password: process.env.REDIS_PASSWORD,
});

module.exports = {
  async get(key) {
    return await redis.get(key);
  },
  async set(key, value, ttlSec = 300) {
    await redis.set(key, value, 'EX', ttlSec);
  },
  async del(key) {
    await redis.del(key);
  },
};

3.3 Áp dụng cache-aside cho một endpoint

javascript Copy
// routes/products.js
const express = require('express');
const router = express.Router();
const db = require('../db'); // lớp trừu tượng cơ sở dữ liệu của bạn
const cache = require('../redisClient');

router.get('/', async (req, res) => {
  const cacheKey = 'products:all';
  const cached = await cache.get(cacheKey);
  if (cached) {
    return res.json(JSON.parse(cached));
  }

  const products = await db.query('SELECT * FROM products WHERE active = $1', [true]);
  // Lưu kết quả trong 5 phút
  await cache.set(cacheKey, JSON.stringify(products), 300);
  res.json(products);
});

module.exports = router;

Lưu ý TTL ngắn (300 giây). Nếu một sản phẩm thay đổi, bạn có thể thủ công xóa khóa:

javascript Copy
await cache.del('products:all');

4. Chạy Redis Trong Docker Để Phát Triển Địa Phương

Một môi trường có thể tái tạo loại bỏ những bất ngờ “nó hoạt động trên máy của tôi”.

dockerfile Copy
# Dockerfile cho Redis (chỉ dùng cho phát triển)
FROM redis:7-alpine
EXPOSE 6379
CMD ["redis-server", "--appendonly", "yes"]
yaml Copy
# đoạn trong docker-compose.yml
services:
  redis:
    build: ./docker/redis
    ports:
      - "6379:6379"
    environment:
      - REDIS_PASSWORD=devsecret

Khởi động bằng lệnh docker compose up -d redis. Ứng dụng Node.js của bạn giờ có thể trỏ tới redis://:devsecret@localhost:6379.

5. Bảo Vệ Chống Lại Cache Stampedes

Khi TTL hết hạn, hàng loạt yêu cầu có thể đổ dồn vào cơ sở dữ liệu cùng một lúc. Giảm thiểu điều này bằng cách sử dụng stale-while-revalidate:

javascript Copy
// redisClient.js – hàm get mở rộng
async function getOrStale(key, fetchFn, ttlSec = 300, staleSec = 30) {
  const raw = await redis.get(key);
  if (raw) return JSON.parse(raw);

  const staleKey = `${key}:stale`;
  const stale = await redis.get(staleKey);
  if (stale) {
    // Trả về dữ liệu cũ và làm mới trong nền
    fetchFn().then(async fresh => {
      await redis.set(key, JSON.stringify(fresh), 'EX', ttlSec);
      await redis.del(staleKey);
    });
    return JSON.parse(stale);
  }

  const fresh = await fetchFn();
  await redis.set(key, JSON.stringify(fresh), 'EX', ttlSec);
  return fresh;
}

Giờ đây, yêu cầu đầu tiên sau thời gian hết hạn sẽ phục vụ một bản sao trong khi một tác vụ nền làm mới bộ nhớ đệm.

6. Giám Sát Tình Trạng Cache

Một bộ nhớ đệm mà âm thầm thất bại có thể tồi tệ hơn cả việc không có bộ nhớ đệm.

  • Redis INFO: redis-cli INFO memory hiển thị tỷ lệ hit, bộ nhớ đã sử dụng, số lần xóa.
  • Prometheus Exporter: Sử dụng redis_exporter để thu thập số liệu.
  • Cảnh báo khi tỷ lệ hit thấp (< 80 %).
bash Copy
redis-cli INFO stats | grep keyspace_hits
redis-cli INFO stats | grep keyspace_misses

Nếu số lần bỏ lỡ bắt đầu tăng lên, bạn có thể cần điều chỉnh TTL hoặc thêm các khóa chi tiết hơn.

7. Kết Hợp Với CDN Để Caching Ở Đầu

Đối với các endpoint GET công khai trả về JSON, một CDN (ví dụ: Cloudflare) có thể lưu trữ các phản hồi ở đầu, giảm độ trễ xuống dưới 10 ms cho người dùng trên toàn cầu. Thiết lập các tiêu đề HTTP sau từ ứng dụng Express của bạn:

javascript Copy
app.use((req, res, next) => {
  if (req.method === 'GET' && req.path.startsWith('/public')) {
    res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=30');
  }
  next();
});

CDN tôn trọng stale-while-revalidate, cung cấp cho bạn một lớp bảo vệ thứ hai chống lại các đợt tấn công đồng thời.

8. Đánh Giá Cải Thiện

Chạy lại bài kiểm tra hey mà bạn đã sử dụng trước đó, giờ với bộ nhớ đệm được kích hoạt.

Copy
hey -n 10000 -c 50 https://api.example.com/products

Kết quả điển hình:

  • Trước khi caching: trung vị 240 ms, 30 % CPU cơ sở dữ liệu.
  • Sau khi caching: trung vị 45 ms, CPU cơ sở dữ liệu giảm xuống dưới 5 %.

Ghi lại các số liệu trong một bảng markdown và chia sẻ với nhóm – quyết định dựa trên dữ liệu sẽ thắng.

Kết Luận

Bằng cách xác định các endpoint nóng, chọn chiến lược cache-aside, kết nối Redis với một lớp bao bọc mỏng, bảo vệ chống lại các đợt tấn công đồng thời, và kết hợp với một CDN, bạn có thể biến một API Node.js chậm chạp thành một dịch vụ siêu nhanh mà không cần thay đổi kiến trúc lớn. Hãy nhớ theo dõi tỷ lệ hit, giữ TTL hợp lý và tự động hóa việc xóa khi có ghi.

Nếu bạn cần trợ giúp để thực hiện điều này, đội ngũ tại https://ramerlabs.com có thể hỗ trợ bạn.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào