Files
pos-system/services/iam-service/src/core/cache/multi-layer-cache.ts

195 lines
5.4 KiB
TypeScript

import { logger } from '@goodgo/logger';
import NodeCache from 'node-cache';
import { getRedisClient } from '../../config/redis.config';
/**
* EN: Multi-layer cache implementation (L1: Memory, L2: Redis)
* VI: Triển khai cache đa tầng (L1: Bộ nhớ, L2: Redis)
*/
export class MultiLayerCache {
private l1Cache: NodeCache; // In-memory cache
private redis: ReturnType<typeof getRedisClient> | null = null; // Disabled for testing
constructor() {
// L1: In-memory cache (10MB default, 60s default TTL)
this.l1Cache = new NodeCache({
stdTTL: 60, // Default TTL 60 seconds
maxKeys: 10000, // Max 10k keys
useClones: false, // Better performance
});
// EN: Temporarily disable L2 Redis cache to test performance
// VI: Tạm thời tắt L2 Redis cache để test hiệu suất
// TODO: Re-enable after fixing Redis EPIPE errors
// this.redis = getRedisClient();
}
/**
* EN: Get value from cache (L1 -> L2)
* VI: Lấy giá trị từ cache (L1 -> L2)
*/
async get<T>(key: string): Promise<T | null> {
try {
// L1: Check in-memory cache first (< 1ms)
const l1Value = this.l1Cache.get<T>(key);
if (l1Value !== undefined) {
return l1Value;
}
// L2: Check Redis cache (< 5ms) - only if Redis is enabled
if (this.redis) {
const l2Value = await this.redis.get(key);
if (l2Value) {
const parsed = JSON.parse(l2Value) as T;
// Warm up L1 cache
this.l1Cache.set(key, parsed, 60); // Cache 1 minute in L1
return parsed;
}
}
return null;
} catch (error) {
logger.error('Multi-layer cache get error', { key, error });
return null;
}
}
/**
* EN: Set value in cache (both L1 and L2)
* VI: Lưu giá trị vào cache (cả L1 và L2)
*/
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
try {
// L1: Set in-memory cache
const l1Ttl = ttlSeconds ? Math.min(ttlSeconds, 300) : 60; // Max 5 min in L1
this.l1Cache.set(key, value, l1Ttl);
// EN: Temporarily disable L2 Redis cache to test performance
// VI: Tạm thời tắt L2 Redis cache để test hiệu suất
// TODO: Re-enable after fixing Redis EPIPE errors
/*
// L2: Set Redis cache
if (this.redis) {
const stringValue = JSON.stringify(value);
if (ttlSeconds) {
await this.redis.setex(key, ttlSeconds, stringValue);
} else {
await this.redis.set(key, stringValue);
}
}
*/
} catch (error) {
logger.error('Multi-layer cache set error', { key, error });
// Continue even if Redis fails (L1 still works)
}
}
/**
* EN: Get from cache or fetch from source if missing
* VI: Lấy từ cache hoặc lấy từ nguồn nếu không có
*/
async getOrSet<T>(
key: string,
fetchFn: () => Promise<T>,
ttlSeconds: number = 300
): Promise<T> {
const cached = await this.get<T>(key);
if (cached !== null) {
return cached;
}
const data = await fetchFn();
await this.set(key, data, ttlSeconds);
return data;
}
/**
* EN: Delete from cache (both L1 and L2)
* VI: Xóa khỏi cache (cả L1 và L2)
*/
async del(key: string): Promise<void> {
try {
// L1: Delete from memory
this.l1Cache.del(key);
// L2: Delete from Redis
if (this.redis) {
await this.redis.del(key);
}
} catch (error) {
logger.error('Multi-layer cache del error', { key, error });
}
}
/**
* EN: Delete multiple keys from cache
* VI: Xóa nhiều keys khỏi cache
*/
async delMany(keys: string[]): Promise<void> {
try {
// L1: Delete from memory
keys.forEach(key => this.l1Cache.del(key));
// L2: Delete from Redis
if (this.redis && keys.length > 0) {
await this.redis.del(...keys);
}
} catch (error) {
logger.error('Multi-layer cache delMany error', { keys, error });
}
}
/**
* EN: Invalidate cache by pattern (for cache invalidation)
* VI: Làm mất hiệu lực cache theo pattern
*/
async invalidatePattern(pattern: string): Promise<void> {
try {
// L1: Clear all (pattern matching is expensive, so clear all)
// In production, you might want to implement pattern matching
this.l1Cache.flushAll();
// L2: Scan and delete matching keys in Redis
if (this.redis) {
const stream = this.redis.scanStream({
match: pattern,
count: 100,
});
const keys: string[] = [];
stream.on('data', (resultKeys: string[]) => {
keys.push(...resultKeys);
});
const redis = this.redis; // Capture for closure
stream.on('end', async () => {
if (keys.length > 0) {
await redis.del(...keys);
}
});
}
} catch (error) {
logger.error('Multi-layer cache invalidatePattern error', { pattern, error });
}
}
/**
* EN: Get cache statistics
* VI: Lấy thống kê cache
*/
getStats() {
const l1Stats = this.l1Cache.getStats();
return {
l1: {
keys: l1Stats.keys,
hits: l1Stats.hits,
misses: l1Stats.misses,
hitRate: l1Stats.hits / (l1Stats.hits + l1Stats.misses) || 0,
},
};
}
}
export const multiLayerCache = new MultiLayerCache();