Redis — Cache, Session, Pub/Sub, Rate Limit, Queue
Redis in-memory data store: cấu trúc dữ liệu, caching patterns, pub/sub, rate limiting, và distributed lock.
Redis là in-memory data store nhanh nhất — dùng để cache, session, pub/sub, queue, rate limiting. Đa năng hơn bạn nghĩ.
Data Structures#
String — Cache Cơ Bản#
SET user:123 '{"name":"Alice"}' EX 3600
GET user:123
INCR page:views
INCRBY counter 5bashList — Queue / Stack#
# Queue (FIFO)
LPUSH notifications:user_123 "New message"
RPOP notifications:user_123
# Stack (LIFO)
LPUSH stack:actions "action1"
LPOP stack:actions
# Range
LRANGE messages:room_abc 0 -1 # Tất cảbashSet — Unique Items#
SADD post:42:likes user_123
SADD post:42:likes user_456
SCARD post:42:likes # 2
SISMEMBER post:42:likes user_123 # 1 (true)
SINTER post:42:likes post:99:likes # IntersectionbashSorted Set — Leaderboard#
ZADD leaderboard 1000 "player_1"
ZADD leaderboard 2500 "player_2"
ZADD leaderboard 1500 "player_3"
ZREVRANGE leaderboard 0 2 WITHSCORES # Top 3
ZRANK leaderboard player_2 # Rank
ZINCRBY leaderboard 500 player_1 # +500 điểmbashHash — Object#
HSET user:123 name "Alice" email "alice@example.com" age 30
HGET user:123 name
HGETALL user:123
HINCRBY user:123 age 1bashCaching Patterns#
Cache Aside (Phổ biến nhất)#
async function getUser(id: number) {
// 1. Check cache
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
// 2. Cache miss → query DB
const user = await db.user.findUnique({ where: { id } });
// 3. Set cache
if (user) {
await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
}
return user;
}typescriptCache Penetration — Khi Data Không Tồn Tại#
async function getUser(id: number) {
const cached = await redis.get(`user:${id}`);
// Cache empty value cho key không tồn tại
if (cached === 'NULL') return null;
if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id } });
if (!user) {
await redis.setex(`user:${id}`, 300, 'NULL'); // Cache 5 phút
return null;
}
await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
return user;
}typescriptCache Breakdown — Khi Key Hết Hạn#
Dùng mutex lock — chỉ một request được query DB:
async function getHotData() {
const cached = await redis.get('hot:data');
if (cached) return JSON.parse(cached);
// Lock — chỉ một request query DB
const lock = await redis.setnx('lock:hot:data', '1');
if (lock) {
await redis.expire('lock:hot:data', 5);
const data = await expensiveQuery();
await redis.setex('hot:data', 60, JSON.stringify(data));
await redis.del('lock:hot:data');
return data;
}
// Wait và retry
await sleep(100);
return getHotData();
}typescriptCache Stampede — Preload Trước Khi Hết Hạn#
async function getConfig() {
const TTL = 3600;
const cached = await redis.get('app:config');
if (cached) {
const ttl = await redis.ttl('app:config');
// Sắp hết hạn? Refresh async
if (ttl < 300) {
refreshConfig(); // Không await
}
return JSON.parse(cached);
}
return refreshConfig();
}
async function refreshConfig() {
const config = await db.config.findMany();
await redis.setex('app:config', 3600, JSON.stringify(config));
return config;
}typescriptSession Store#
import session from 'express-session';
import RedisStore from 'connect-redis';
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
maxAge: 24 * 60 * 60 * 1000, // 24h
},
}));typescriptPub/Sub — Real-time Message#
// Publisher
await redis.publish('channel:notifications', JSON.stringify({
type: 'order.created',
orderId: 123,
userId: 456,
}));
// Subscriber
const subscriber = redis.duplicate();
await subscriber.subscribe('channel:notifications', (message) => {
const event = JSON.parse(message);
console.log('Received:', event);
// Gửi WebSocket cho user, send email, etc.
});typescriptRate Limiting — Sliding Window#
async function checkRateLimit(key: string, limit: number, windowSec: number) {
const now = Date.now();
const windowStart = now - windowSec * 1000;
const multi = redis.multi();
multi.zRemRangeByScore(key, 0, windowStart); // Xoá entries cũ
multi.zCard(key); // Đếm entries
multi.zAdd(key, { score: now, value: `${now}` });
multi.expire(key, windowSec);
const results = await multi.exec();
const count = results[1][1] as number;
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: Math.ceil((windowStart + windowSec * 1000) / 1000),
};
}
// Usage
app.use('/api/auth', async (req, res, next) => {
const { allowed, remaining } = await checkRateLimit(req.ip, 10, 60);
res.set('X-RateLimit-Remaining', remaining.toString());
if (!allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
next();
});typescriptDistributed Lock#
async function withLock<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {
const lockKey = `lock:${key}`;
const lockValue = crypto.randomUUID();
const acquired = await redis.set(lockKey, lockValue, 'PX', ttl, 'NX');
if (!acquired) {
throw new Error('Could not acquire lock');
}
try {
return await fn();
} finally {
// Lua script — atomic unlock
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
await redis.eval(script, 1, lockKey, lockValue);
}
}
// Usage
await withLock('payment:order_123', 5000, async () => {
// Chỉ một process xử lý payment
await processPayment(orderId);
});typescriptQueue — BullMQ#
import { Queue, Worker } from 'bullmq';
const emailQueue = new Queue('email', { connection: redis });
// Producer
await emailQueue.add('send-welcome', {
to: 'alice@example.com',
template: 'welcome',
userId: 123,
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
// Consumer
const worker = new Worker('email', async (job) => {
const { to, template } = job.data;
await sendEmail(to, template);
console.log(`Email sent to ${to}`);
}, { connection: redis });
worker.on('completed', (job) => console.log(`Job ${job.id} completed`));
worker.on('failed', (job, err) => console.error(`Job ${job.id} failed:`, err));typescriptHyperLogLog — Đếm Unique#
# Đếm unique visitors — chỉ dùng 12KB, sai số ~2%
PFADD page:visits:2024-01-01 ip_1 ip_2 ip_3
PFADD page:visits:2024-01-01 ip_1 ip_4
PFCOUNT page:visits:2024-01-01 # 4
PFMERGE page:visits:2024-01 page:visits:2024-01-01 page:visits:2024-01-02bashPersistence#
# RDB — snapshot định kỳ (mặc định)
save 900 1 # 15 phút nếu có 1 thay đổi
save 300 10 # 5 phút nếu có 10 thay đổi
save 60 10000 # 1 phút nếu có 10000 thay đổi
# AOF — log mọi write (chậm hơn nhưng an toàn hơn)
appendonly yes
appendfsync everysec # Sync mỗi giâybashKết Luận#
Redis đa năng hơn “cache đơn thuần”:
| Use case | Cấu trúc |
|---|---|
| Cache | String + TTL |
| Session | String hoặc Hash |
| Rate limiting | Sorted Set + ZRem |
| Queue | List hoặc BullMQ |
| Pub/Sub | Pub/Sub channels |
| Distributed lock | String + NX + Lua |
| Leaderboard | Sorted Set |
| Unique counter | HyperLogLog |
Install: brew install redis hoặc Docker. Production: Redis Cluster hoặc managed (Upstash, Redis Cloud).