Bạn có biết?
Ứng dụng của bạn chạy ngon ở môi trường dev, nhưng khi lên production — Redis vừa restart, vừa network hiccup — mọi request đột nhiên treo 30 giây rồi 500. Vấn đề không nằm ở Redis, mà nằm ở cách bạn quản lý kết nối. Bài này sẽ dạy bạn cấu hình kết nối Redis trong Node.js đúng cách để ứng dụng sống khỏe dưới mọi tình huống.
Node.js và kết nối Redis: hiểu đúng mô hình
Điều đầu tiên cần hiểu: một instance ioredis = một kết nối TCP duy nhất, chia sẻ cho toàn bộ ứng dụng. Node.js chạy đơn luồng, mọi request await trên cùng kết nối đó — đây là thiết kế chuẩn, KHÔNG cần connection pool kiểu Java (nhiều kết nối vật lý). Điều bạn thực sự cần quản lý là: retry, backoff, và trạng thái kết nối.
Cấu hình reconnect thông minh
import Redis from "ioredis";
const redis = new Redis({
host: process.env.REDIS_HOST,
port: 6379,
password: process.env.REDIS_PASSWORD,
// ---- Retry mỗi request ----
maxRetriesPerRequest: 3, // mỗi request thử tối đa 3 lần
enableReadyCheck: true, // chỉ nhận lệnh khi Redis sẵn sàng (LOADING xong)
// ---- Reconnect với exponential backoff ----
retryStrategy(times) {
if (times > 20) return null; // bỏ cuộc sau 20 lần — để health check quyết định
return Math.min(times * 200, 5000); // 200ms, 400ms, 800ms... tối đa 5s
},
// ---- Giữ kết nối sống ----
keepAlive: 5000, // gửi TCP keepalive mỗi 5s
connectTimeout: 10_000, // timeout kết nối ban đầu
disconnectTimeout: 5000, // chờ cleanup khi đóng
lazyConnect: false, // kết nối ngay khi khởi tạo
});
Giải thích các option quan trọng:
retryStrategy— quyết định delay giữa các lần reconnect. Trả vềnullđể dừng hẳn (tránh spam kết nối khi Redis chết lâu)maxRetriesPerRequest— số lần ioredis tự retry một request khi kết nối đứt. Quá cao → request treo lâu; quá thấp → fail nhanh, dễ 500enableReadyCheck— đợi Redis qua giai đoạn LOADING (đang nạp RDB/AOF) mới nhận lệnh, tránh lỗi "READONLY/LOADING" cho clientkeepAlive— chống kết nối chết âm thầm (idle timeout của firewall/load balancer)
Fail nhanh, không fail im lặng
Nguyên tắc vàng với cache: Redis hỏng không được làm hỏng request chính. Cache-aside pattern yêu cầu fail-fast và fallback:
const redis = new Redis({ maxRetriesPerRequest: 1, retryStrategy: () => null });
async function getFromCache(key) {
try {
return await redis.get(key);
} catch (err) {
// Redis lỗi → log + trả null, request đi thẳng xuống database
console.error("[cache] failed:", err.message);
return null;
}
}
// Circuit breaker đơn giản: sau 5 lỗi liên tiếp, ngừng gọi Redis 30s
let failures = 0;
let openUntil = 0;
async function getFromCacheSafe(key) {
if (Date.now() < openUntil) return null; // circuit open — bỏ qua cache
try {
const value = await redis.get(key);
failures = 0;
return value;
} catch (err) {
if (++failures >= 5) openUntil = Date.now() + ;
;
}
}
Với pattern này, Redis down = request vẫn trả dữ liệu từ database, chỉ chậm hơn chút. Hệ thống "degrade gracefully" thay vì sập theo Redis. Chi tiết đầy đủ trong bài Cache Strategies.
redis.on("connecting", () => log("connecting..."));
redis.on("connect", () => log("TCP connected"));
redis.on("ready", () => { log("ready — nhận lệnh bình thường"); failures = 0; });
redis.on("reconnecting", (delay) => log(`reconnecting in ${delay}ms`));
redis.on("error", (err) => log("error:", err.message)); // BẮT BUỘC — thiếu listener error là crash
redis.on("close", () => log("connection closed"));
redis.on("end", () => log("kết nối kết thúc hẳn — cần alert!"));
// Health check định kỳ: ping + đo latency
setInterval(async () => {
const start = Date.now();
try {
await redis.ping();
const latency = Date.now() - start;
if (latency > 100) log(`redis latency cao: ${latency}ms`); // alert
} catch {
log("redis DOWN — kiểm tra ngay!"); // alert
}
}, 30_000);
// Graceful shutdown: đóng kết nối sạch khi app dừng
async function shutdown(signal) {
log(`nhận ${signal}, đang tắt...`);
await redis.quit(); // chờ mọi lệnh pending xong rồi đóng
process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
// Sentinel: ioredis tự tìm master hiện tại, tự chuyển khi failover
const sentinel = new Redis({
sentinels: [
{ host: "sentinel-1", port: 26379 },
{ host: "sentinel-2", port: 26379 },
{ host: "sentinel-3", port: 26379 },
],
name: "mymaster",
retryStrategy: (times) => Math.min(times * 200, 5000),
});
// Cluster: truyền nhiều node, ioredis tự học bản đồ slot
const cluster = new Redis.Cluster(
[
{ host: "redis-1", port: 6379 },
{ host: "redis-2", port: 6379 },
{ host: "redis-3", port: 6379 },
],
{
scaleReads: "slave",
maxRedirections: 16,
retryDelayOnFailover: 100,
retryDelayOnClusterDown: 1000,
}
);
- Tạo Redis mới trong mỗi request — tốn kết nối, chậm, rò rỉ. Tạo MỘT instance, export module
- Không có error listener — crash process bất ngờ
- maxRetriesPerRequest quá cao — request treo lâu khi Redis down; 1-3 là đủ cho cache
- RetryStrategy trả về hằng số nhỏ mãi mãi — spam reconnect; dùng backoff tăng dần và giới hạn lần thử
- Không phân biệt cache và dữ liệu bắt buộc — cache fail nhanh + fallback; dữ liệu quan trọng (lock, hàng đợi) mới cần retry mạnh
- Quên graceful shutdown — deploy kill process giữa chừng, mất lệnh pending; luôn quit() khi nhận SIGTERM
Quản lý kết nối Redis trong Node.js không phải là "pool nhiều kết nối" mà là quản lý trạng thái và thất bại: retryStrategy với backoff tăng dần, maxRetriesPerRequest vừa phải, error listener bắt buộc, health check định kỳ, graceful shutdown, và pattern fail-fast + fallback cho cache. Áp dụng đủ những điều này, Redis của bạn có thể "ngã" mà ứng dụng vẫn đứng vững.
0 bình luận
Đang tải bình luận...
Để lại bình luận