@fastify-core/redis
v1.0.0
Published
TypeScript Redis cache wrapper with TTL, prefix support, and type-safe API
Downloads
87
Maintainers
Readme
@fasify-core/redis
Redis cache wrapper được xây dựng bằng TypeScript với hỗ trợ TTL, distributed lock, prefix, và type-safe API.
✨ Tính năng chính:
- ✅ Type-safe với TypeScript
- ✅ TTL (Time To Live) cho cache
- ✅ Prefix namespace cho key
- ✅ Auto serialize/deserialize JSON
- ✅ Distributed Lock (ngăn thundering herd)
- ✅ Local single-flight (cache request deduplication)
- ✅ Factory pattern (getOrSet/remember)
- ✅ Connection pooling & auto-renewal
- ✅ Comprehensive error handling
📦 Cài đặt
npm install @fasify-core/redis
# hoặc
pnpm add @fasify-core/redis
# hoặc
yarn add @fasify-core/redisYêu cầu: Node.js >= 24.x, Redis >= 6.x
🚀 Quick Start
1. Khởi tạo đơn giản
import { RedisCache } from '@fasify-core/redis';
const cache = new RedisCache();
await cache.connect();
// Lưu dữ liệu
await cache.set('user:1', { id: 1, name: 'John' }, { ttl: 3600 });
// Lấy dữ liệu
const user = await cache.get('user:1');
// Đóng kết nối
await cache.disconnect();2. Cấu hình đầy đủ
const cache = new RedisCache({
// URL kết nối Redis
url: 'redis://:password@localhost:6379',
// Prefix cho tất cả key (optional)
prefix: 'myapp:',
// TTL mặc định cho cache (giây)
defaultTTL: 3600,
// Bật distributed lock cho getOrSet()
distributedLock: true,
// TTL của lock (giây, mặc định 30s)
lockTTL: 30,
// Delay giữa các lần retry lock (ms, mặc định 10ms)
lockRetryDelay: 10,
// Số lần retry tối đa (mặc định 50)
lockRetryAttempts: 50,
// Jitter random để tránh lock conflict (ms, mặc định 50ms)
lockRetryJitter: 50,
// Tùy chọn Redis client
clientOptions: {
// ...Redis client options
}
});
await cache.connect();📖 API Reference
| Method | Mô tả |
|--------|-------|
| connect() | Kết nối đến Redis server |
| disconnect() | Đóng kết nối Redis |
| set<T>(key, value, options?) | Lưu dữ liệu với TTL tùy chọn |
| get<T>(key) | Lấy dữ liệu, trả về null nếu không tồn tại |
| has(key) | Kiểm tra key có tồn tại |
| update<T>(key, value, options?) | Cập nhật giá trị cache |
| delete(key) | Xóa một key |
| deleteMany(keys) | Xóa nhiều key cùng lúc |
| ttl(key) | Lấy TTL còn lại (giây) |
| clear() | Xóa tất cả key theo prefix |
| getOrSet<T>(key, factory, options?) | Cache-aside + lock |
| remember<T>(key, factory, options?) | Alias của getOrSet |
| getClient() | Truy cập Redis client trực tiếp |
💡 Hướng dẫn chi tiết
💡 Hướng dẫn chi tiết
1️⃣ Lưu dữ liệu vào cache
// Lưu string
await cache.set('username', 'john');
// Lưu object
await cache.set('user', { id: 1, name: 'John', email: '[email protected]' });
// Lưu với TTL tùy chỉnh (300 giây)
await cache.set('session:abc123', { token: 'xxx', userId: 1 }, { ttl: 300 });
// Lưu mà không có TTL (permanent)
await cache.set('config:app', { version: '1.0.0' });
// Sử dụng TTL mặc định từ constructor
const cache = new RedisCache({ defaultTTL: 3600 });
await cache.set('data', { foo: 'bar' }); // Auto TTL 3600s2️⃣ Lấy dữ liệu từ cache
// Lấy dữ liệu cơ bản
const username = await cache.get<string>('username');
console.log(username); // 'john'
// Lấy object với type-safe
interface User {
id: number;
name: string;
email: string;
}
const user = await cache.get<User>('user');
console.log(user?.name); // 'John'
// Nếu key không tồn tại, trả về null
const notFound = await cache.get<User>('user:999');
console.log(notFound); // null
// Ép kiểu
const data = await cache.get<any>('anything');3️⃣ Kiểm tra cache tồn tại
const exists = await cache.has('username');
if (exists) {
console.log('Cache found!');
}
// Kết hợp với get
const user = await cache.get<User>('user');
if (user) {
// Sử dụng dữ liệu
}4️⃣ Cập nhật cache
// Cập nhật với TTL mặc định
await cache.update('user', { id: 1, name: 'Jane' });
// Cập nhật với TTL tùy chỉnh
await cache.update('user', { id: 1, name: 'Jane' }, { ttl: 1800 });5️⃣ Xóa cache
// Xóa một key
const deleted = await cache.delete('user');
console.log(deleted); // true nếu xóa thành công, false nếu key không tồn tại
// Xóa nhiều key
const count = await cache.deleteMany(['user', 'session', 'config']);
console.log(count); // 2 (số lượng key đã xóa)
// Xóa tất cả cache theo prefix
const cache = new RedisCache({ prefix: 'myapp:' });
const deletedAll = await cache.clear();
console.log(deletedAll); // tổng số key đã xóa6️⃣ Lấy TTL của key
const ttl = await cache.ttl('session');
// Kết quả:
// - số dương: thời gian sống còn (giây)
// - -1: key tồn tại nhưng không có TTL
// - -2: key không tồn tại
if (ttl > 0) {
console.log(`Cache sẽ hết hạn trong ${ttl}s`);
} else if (ttl === -1) {
console.log('Cache tồn tại vĩnh viễn');
} else {
console.log('Cache không tồn tại');
}7️⃣ Distributed Locking (⭐ Advanced)
Vấn đề: Khi nhiều request cùng lúc truy vấn data không có trong cache, tất cả sẽ gọi database → Thundering Herd Problem
Giải pháp: Sử dụng getOrSet() với distributed lock
// Nếu cache miss, chỉ 1 request sẽ gọi factory
// Request khác sẽ chờ kết quả từ request đầu tiên
const user = await cache.getOrSet<User>(
'user:1',
async () => {
console.log('Fetching from database...');
const data = await db.users.findById(1);
return data;
},
{ ttl: 3600 }
);
// Hoặc sử dụng alias 'remember'
const user = await cache.remember<User>(
'user:1',
async () => {
return await db.users.findById(1);
},
{ ttl: 3600 }
);Cách hoạt động:
- Request A: Cache miss → acquire lock → gọi factory → lưu cache → release lock
- Request B,C,D: Cache miss → chờ lock → lấy cache từ Request A
- Request E: Cache hit → trả về dữ liệu
// Ví dụ: Fetch API từ external service
const githubUser = await cache.remember<GitHubUser>(
`github:${username}`,
async () => {
const res = await fetch(`https://api.github.com/users/${username}`);
if (!res.ok) throw new Error('GitHub API error');
return res.json();
},
{ ttl: 1800 } // Cache 30 phút
);8️⃣ Override lock options per-call
// Bật lock cho call này (mặc định từ constructor)
const data = await cache.getOrSet(
'expensive:data',
() => computeExpensive(),
{
ttl: 3600,
lock: true, // Override
lockTTL: 60, // TTL lock khác
lockRetryDelay: 20, // Delay giữa retry khác
lockRetryAttempts: 100, // Attempts khác
lockRetryJitter: 100 // Jitter khác
}
);9️⃣ Truy cập Redis client trực tiếp
const client = cache.getClient();
// Sử dụng Redis commands không được wrap
await client.incrby('counter', 1);
await client.lpush('queue', 'item1', 'item2');
const len = await client.llen('queue');
// Thực hiện transaction
const result = await client
.multi()
.set('key1', 'value1')
.set('key2', 'value2')
.exec();🔟 Connection Lifecycle
import { RedisCache } from '@fasify-core/redis';
const cache = new RedisCache({ prefix: 'myapp:' });
try {
// Kết nối lần đầu
await cache.connect();
// Sử dụng cache
const data = await cache.get('key');
} catch (error) {
console.error('Redis error:', error);
} finally {
// Luôn đóng kết nối khi xong
await cache.disconnect();
}
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('Shutting down...');
await cache.disconnect();
process.exit(0);
});🎯 Real-world Examples
📝 Caching User Data với Invalidation
import { RedisCache } from '@fasify-core/redis';
const cache = new RedisCache({ prefix: 'users:', defaultTTL: 1800 });
await cache.connect();
interface User {
id: number;
name: string;
email: string;
role: string;
}
// Lấy user với caching
async function getUser(userId: number): Promise<User | null> {
return cache.remember<User>(
`user:${userId}`,
async () => {
// Fetch từ database nếu cache miss
const user = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
return user;
},
{ ttl: 3600 } // Cache 1 giờ
);
}
// Cập nhật user và invalidate cache
async function updateUser(userId: number, data: Partial<User>): Promise<User> {
// Update database
const user = await db.query(
'UPDATE users SET ? WHERE id = $1 RETURNING *',
[data, userId]
);
// Xóa cache
await cache.delete(`user:${userId}`);
return user;
}
// Xóa user và cache
async function deleteUser(userId: number): Promise<void> {
await db.query('DELETE FROM users WHERE id = $1', [userId]);
await cache.delete(`user:${userId}`);
}
// Sử dụng
const user = await getUser(1);
console.log(user);
await updateUser(1, { name: 'New Name' });🔐 Caching Session/Auth
const cache = new RedisCache({ prefix: 'session:' });
await cache.connect();
interface Session {
userId: number;
token: string;
expiresAt: number;
}
async function createSession(userId: number): Promise<Session> {
const session: Session = {
userId,
token: generateToken(),
expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24 hours
};
// Cache session với TTL 24 giờ
await cache.set(session.token, session, { ttl: 86400 });
return session;
}
async function getSession(token: string): Promise<Session | null> {
return cache.get<Session>(token);
}
async function revokeSession(token: string): Promise<void> {
await cache.delete(token);
}🌐 Caching API Responses
const cache = new RedisCache({ prefix: 'api:' });
await cache.connect();
interface GitHubUser {
login: string;
name: string;
public_repos: number;
}
async function fetchGitHubUser(username: string): Promise<GitHubUser> {
return cache.remember<GitHubUser>(
`github:${username}`,
async () => {
const res = await fetch(`https://api.github.com/users/${username}`);
if (!res.ok) {
throw new Error(`GitHub API error: ${res.statusText}`);
}
return res.json();
},
{ ttl: 3600 } // Cache 1 giờ
);
}
// Ví dụ sử dụng
const user = await fetchGitHubUser('torvalds');
console.log(`${user.name} has ${user.public_repos} public repos`);📊 Rate Limiting Counter
const cache = new RedisCache({ prefix: 'ratelimit:' });
await cache.connect();
async function checkRateLimit(userId: number, limit: number, window: number): Promise<boolean> {
const key = `user:${userId}`;
// Lấy client để sử dụng INCR
const client = cache.getClient();
const count = await client.incr(key);
// Đặt TTL nếu là lần đầu
if (count === 1) {
await client.expire(key, window);
}
return count <= limit;
}
// Sử dụng
const allowed = await checkRateLimit(123, 100, 3600); // 100 requests per hour
if (!allowed) {
return res.status(429).send('Too many requests');
}⚙️ Type Definitions
RedisCacheOptions
interface RedisCacheOptions {
// Redis URL (default: redis://localhost:6379)
url?: string;
// Prefix cho tất cả key
prefix?: string;
// TTL mặc định (giây)
defaultTTL?: number;
// Redis client options
clientOptions?: RedisClientOptions;
// Bật distributed lock (default: false)
distributedLock?: boolean;
// TTL của lock (giây, default: 30)
lockTTL?: number;
// Delay giữa retry (ms, default: 10)
lockRetryDelay?: number;
// Max attempts (default: 50)
lockRetryAttempts?: number;
// Random jitter (ms, default: 50)
lockRetryJitter?: number;
}SetOptions
interface SetOptions {
// TTL cho operation này (override defaultTTL)
ttl?: number;
}GetOrSetOptions
interface GetOrSetOptions extends SetOptions {
// Override distributedLock
lock?: boolean;
// Override lock TTL
lockTTL?: number;
// Override retry delay
lockRetryDelay?: number;
// Override attempts
lockRetryAttempts?: number;
// Override jitter
lockRetryJitter?: number;
}🛡️ Error Handling
import { RedisCache } from '@fasify-core/redis';
const cache = new RedisCache();
try {
await cache.connect();
const data = await cache.get('key');
if (!data) {
console.log('Cache miss');
}
} catch (error) {
console.error('Redis error:', error);
// Xử lý lỗi
if (error instanceof Error) {
if (error.message.includes('ECONNREFUSED')) {
console.error('Redis server không khả dụng');
}
}
} finally {
await cache.disconnect();
}📋 Best Practices
1. Luôn sử dụng prefix để namespace
// ✅ Good
const userCache = new RedisCache({ prefix: 'users:' });
const postCache = new RedisCache({ prefix: 'posts:' });
// ❌ Bad - Dễ conflict
const cache1 = new RedisCache();
const cache2 = new RedisCache();2. Chọn TTL phù hợp
// Static data: 1-24 hours
await cache.set('config', data, { ttl: 86400 });
// Semi-static: 10-60 minutes
await cache.set('user:profile', data, { ttl: 1800 });
// Dynamic: 1-5 minutes
await cache.set('user:settings', data, { ttl: 300 });
// Real-time: No TTL or very short
await cache.set('live:counter', data, { ttl: 10 });3. Invalidate cache khi update data
async function updateUser(id: number, data: any) {
// Update database
await db.users.update(id, data);
// Invalidate cache
await cache.delete(`user:${id}`);
}4. Sử dụng getOrSet để tránh thundering herd
// ✅ Good - Tự động lock
const data = await cache.remember('expensive', async () => {
return await expensiveOperation();
});
// ❌ Bad - Tất cả request gọi DB
const cached = await cache.get('expensive');
if (!cached) {
const data = await expensiveOperation();
await cache.set('expensive', data);
}5. Bật distributed lock cho concurrent traffic
const cache = new RedisCache({
distributedLock: true, // Bật lock
lockTTL: 30,
lockRetryAttempts: 50
});6. Graceful shutdown
let cache: RedisCache;
async function start() {
cache = new RedisCache();
await cache.connect();
}
async function stop() {
if (cache) {
await cache.disconnect();
}
}
// Hook vào process
process.on('SIGTERM', stop);
process.on('SIGINT', stop);📚 Dependencies
redis^4.0.0
📄 License
MIT
🤝 Contributing
Contributions welcome! Vui lòng submit PR.
Cần giúp đỡ? Mở GitHub Issues
