Assist_Design/apps/bff/src/infra/cache/cache.service.ts
barsa 6d327d3ede Refactor CacheService and MappingCacheService for improved performance and functionality
- Enhanced CacheService by implementing a more efficient pattern deletion method using a pipeline for batch processing.
- Added new methods in CacheService to count keys and calculate memory usage by pattern, improving cache management capabilities.
- Updated MappingCacheService to asynchronously retrieve cache statistics, ensuring accurate reporting of total keys and memory usage.
- Refactored existing methods for better readability and maintainability, including the introduction of a private scanPattern method for key scanning.
2025-11-18 11:03:25 +09:00

143 lines
3.5 KiB
TypeScript

import { Inject, Injectable } from "@nestjs/common";
import Redis from "ioredis";
import { Logger } from "nestjs-pino";
@Injectable()
export class CacheService {
constructor(
@Inject("REDIS_CLIENT") private readonly redis: Redis,
@Inject(Logger) private readonly logger: Logger
) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
if (!value) {
return null;
}
try {
return JSON.parse(value) as T;
} catch (error) {
this.logger.warn({ key, error }, "Failed to parse cached value; evicting entry");
await this.redis.del(key);
return null;
}
}
async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
const serialized = JSON.stringify(value);
if (ttlSeconds !== undefined) {
const ttl = Math.max(0, Math.floor(ttlSeconds));
if (ttl > 0) {
await this.redis.set(key, serialized, "EX", ttl);
} else {
// Allow callers to request immediate expiry without leaking stale values
await this.redis.set(key, serialized, "PX", 1);
}
return;
}
await this.redis.set(key, serialized);
}
async del(key: string): Promise<void> {
await this.redis.del(key);
}
async delPattern(pattern: string): Promise<void> {
const pipeline = this.redis.pipeline();
let pending = 0;
const flush = async () => {
if (pending === 0) {
return;
}
await pipeline.exec();
pending = 0;
};
await this.scanPattern(pattern, async keys => {
keys.forEach(key => {
pipeline.del(key);
pending += 1;
});
if (pending >= 1000) {
await flush();
}
});
await flush();
}
async countByPattern(pattern: string): Promise<number> {
let total = 0;
await this.scanPattern(pattern, keys => {
total += keys.length;
});
return total;
}
async memoryUsageByPattern(pattern: string): Promise<number> {
let total = 0;
await this.scanPattern(pattern, async keys => {
const pipeline = this.redis.pipeline();
keys.forEach(key => {
pipeline.memory("usage", key);
});
const results = await pipeline.exec();
if (!results) {
return;
}
results.forEach(result => {
if (!result) {
return;
}
const [error, usage] = result as [Error | null, unknown];
if (!error && typeof usage === "number") {
total += usage;
}
});
});
return total;
}
async exists(key: string): Promise<boolean> {
return (await this.redis.exists(key)) === 1;
}
buildKey(prefix: string, userId: string, ...parts: string[]): string {
return [prefix, userId, ...parts].join(":");
}
async getOrSet<T>(key: string, fetcher: () => Promise<T>, ttlSeconds: number = 300): Promise<T> {
const cached = await this.get<T>(key);
if (cached !== null) {
return cached;
}
const fresh = await fetcher();
await this.set(key, fresh, ttlSeconds);
return fresh;
}
private async scanPattern(
pattern: string,
onKeys: (keys: string[]) => Promise<void> | void
): Promise<void> {
let cursor = "0";
do {
const [next, keys] = (await this.redis.scan(
cursor,
"MATCH",
pattern,
"COUNT",
1000
)) as unknown as [string, string[]];
cursor = next;
if (keys && keys.length) {
await onKeys(keys);
}
} while (cursor !== "0");
}
}