Redis Cache, Distributed Lock, và Short-term Data — Hướng dẫn sử dụng @RedisCache và @RedisLock decorators.
Module @core/redis cung cấp 2 decorator chính:
| Decorator | Mục đích | Import |
|---|---|---|
@RedisCache | Cache-aside (GET) + Write-through invalidation (SET) | import { RedisCache } from '@core/redis'; |
@RedisLock | Distributed lock — đảm bảo chỉ 1 instance chạy tại 1 thời điểm | import { RedisLock } from '@core/redis'; |
Ngoài ra, RedisClientProxy cung cấp API trực tiếp (get/set/del/setnx/incr...) khi decorator không phù hợp.
interface UseCacheOptions {
ttl?: number; // Seconds, default: 3600
mode?: 'GET' | 'SET'; // default: 'GET'
pattern?: string; // Key pattern, hỗ trợ ${placeholder}
parameters?: Record<string, string>; // Map placeholder → arg path
keyGenerator?: (args: any[]) => string | string[]; // Custom key function
relatedPatterns?: Array<RedisKeyOptions>; // Danh sách pattern cần clear khi mode=SET
alg?: 'MD5' | 'None'; // Hash algorithm cho key, default: None
prefix?: string; // Override prefix (default: Redis client prefix)
delimiter?: string; // default: ':'
connection?: RedisProviderOptions; // Chọn Redis connection cụ thể
}
Flow: Check cache → Hit? Return cache : Execute method → Store result → Return result
@RedisCache({
pattern: 'MASTER_DATA:BUSINESS_TYPE',
ttl: 24 * 60, // 24 phút
})
async query(query: FilterDto): Promise<any> {
return this.repository.findAllAndCount(query);
}
Flow: Execute method → Clear related cache keys → Return result
@RedisCache({
mode: 'SET',
relatedPatterns: [
{
pattern: 'FORM:{formTemplateId}:{formId}',
parameters: {
formTemplateId: 'args[0].formTemplateId',
formId: 'args[0].formId',
},
},
{ pattern: 'FORM:*' }, // Wildcard — clear all FORM cache
],
})
async execute(input: CreateFormDto): Promise<Form> {
return this.formRepository.persistData(input);
}
@RedisCache({
pattern: 'FIND_ONE:{formId}:{groupId}:{fieldId}',
parameters: {
formId: 'args[0].formId', // lấy từ argument[0].formId
groupId: 'args[0].groupId',
fieldId: 'args[0].id',
},
ttl: 3600,
})
async findByInput(input: FormFieldRequestDto): Promise<FormField> { ... }
@RedisCache({
keyGenerator: (args) => `USER:${args[0].id}:${args[0].type}`,
})
async getUser(filter: UserFilter): Promise<User> { ... }
${placeholder} — Recommended{placeholder} — Tương thích{{placeholder}} — Tương thíchDùng khi chỉ cần clear cache mà không cần cache result:
@RedisCache({
mode: 'SET',
relatedPatterns: [{ pattern: 'FORM:*' }],
})
private async invalidateCache(): Promise<void> {}
Đảm bảo chỉ 1 instance (1 pod) chạy đoạn code tại 1 thời điểm.
interface RedisLockOptions {
pattern?: string; // Lock key, default: 'LOCK:'
ttl?: number; // Lock TTL seconds, default: 30
maxRetries?: number; // Số lần retry nếu lock bị chiếm, default: 0 (không retry)
retryDelay?: number; // Delay giữa các retry (ms), default: 5000
parameters?: Record<string, string>;
keyGenerator?: (args: any[]) => string;
connection?: RedisProviderOptions;
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
@RedisLock({
pattern: 'LOCK:MARITAL_STATUS_SYNC',
ttl: 10, // Lock giữ tối đa 10s
maxRetries: 0, // Không retry — nếu pod khác đang chạy thì bỏ qua
})
async execute() {
await this.syncService.execute();
}
| maxRetries | Behavior |
|---|---|
0 | Fire-and-forget: Nếu lock bị chiếm → skip silently |
> 0 | Retry: Chờ retryDelay ms rồi thử lại, tối đa maxRetries lần |
| Lock hết TTL | Lock tự động release (tránh deadlock) |
Khi decorator không phù hợp (ví dụ: idempotency, counter, custom logic):
import { InjectRedis, RedisClientProxy } from '@core/redis';
@Injectable()
export class MyService {
constructor(@InjectRedis() private readonly redis: RedisClientProxy) {}
async example() {
await this.redis.set('KEY', value, { EX: 60 });
const data = await this.redis.get('KEY');
await this.redis.del('KEY');
const acquired = await this.redis.setnx('LOCK:X', 'locked', { EX: 30 });
await this.redis.incr('COUNTER:VIEWS');
}
}
| Tình huống | Decorator/API | Ghi chú |
|---|---|---|
| Master data ít thay đổi | @RedisCache GET, TTL dài | Business types, relation types, enums |
| Query kết quả lặp lại | @RedisCache GET | Repository findById, findByFilter |
| Write → Clear cache | @RedisCache SET | Khi update/create/delete cần invalidate GET cache tương ứng |
| Cronjob multi-pod | @RedisLock | Đảm bảo chỉ 1 pod chạy cronjob |
| Idempotency key | RedisClientProxy set/get | Prevent duplicate requests |
| Rate limiting | RedisClientProxy incr+EX | Counter per user/IP |
| Short-term session data | RedisClientProxy set | Report progress, export status |
MODULE:ENTITY:${id} hoặc MODULE:ACTION:${params}relatedPatterns khi dùng mode SET — tránh stale cacheparameters thay vì keyGenerator khi có thể — dễ đọc và maintainmaxRetries: 0 cho cronjob — skip nếu pod khác đang chạy, không chờRedisClientProxy trực tiếp — wrap try/catch, proceed nếu Redis down| Triệu chứng | Nguyên nhân | Giải pháp |
|---|---|---|
| Cache không tạo | Redis connection chưa ready | Kiểm tra LibRedisModule đã import trong module |
| Cache không clear | relatedPatterns pattern sai | Debug log [CLEANUP] Patterns to clear: |
| Key sai format | parameters path sai | Kiểm tra args[0].field mapping |
| Lock không release | Method throw error trước finally | @RedisLock có finally block tự release |
| Lock conflict liên tục | TTL quá dài | Giảm TTL phù hợp với thời gian xử lý thực tế |