redis pt 2: read cache wiring & event based invalidations

This commit is contained in:
2026-03-30 18:18:46 +08:00
parent c2bafc5bb1
commit d12d3e1ec7
14 changed files with 475 additions and 8 deletions

61
src/common/cache/cache-keys.ts vendored Normal file
View File

@@ -0,0 +1,61 @@
const EMPTY_VALUE_TOKEN = '_';
export const CACHE_NAMESPACE = {
FRIENDS_LIST: 'friends-list',
DOLLS_LIST: 'dolls-list',
USERS_SEARCH: 'users-search',
} as const;
function normalizeKeyPart(value: string | undefined): string {
if (!value) {
return EMPTY_VALUE_TOKEN;
}
return encodeURIComponent(value);
}
export const CACHE_TTL_SECONDS = {
FRIENDS_LIST: 30,
DOLLS_LIST: 30,
USERS_SEARCH: 20,
} as const;
export function friendsListCacheKey(userId: string): string {
return normalizeKeyPart(userId);
}
export function friendsListOwnerTag(userId: string): string {
return `owner:${normalizeKeyPart(userId)}`;
}
export function friendsListDependsOnUserTag(userId: string): string {
return `depends-on:${normalizeKeyPart(userId)}`;
}
export function dollsListCacheKey(
ownerId: string,
requesterId: string,
): string {
return `${normalizeKeyPart(ownerId)}:${normalizeKeyPart(requesterId)}`;
}
export function dollsListOwnerTag(ownerId: string): string {
return `owner:${normalizeKeyPart(ownerId)}`;
}
export function dollsListViewerTag(viewerId: string): string {
return `viewer:${normalizeKeyPart(viewerId)}`;
}
export function usersSearchCacheKey(
username: string | undefined,
excludeUserId: string | undefined,
): string {
return `${normalizeKeyPart(username?.trim().toLowerCase())}:${normalizeKeyPart(excludeUserId)}`;
}
export const USERS_SEARCH_GLOBAL_TAG = 'global';
export function usersSearchUserTag(userId: string): string {
return `user:${normalizeKeyPart(userId)}`;
}

66
src/common/cache/cache-tags.service.ts vendored Normal file
View File

@@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { CacheService } from './cache.service';
const CACHE_TAG_SET_TTL_SECONDS = 86_400;
@Injectable()
export class CacheTagsService {
constructor(private readonly cacheService: CacheService) {}
async rememberKeyForTag(
namespace: string,
tag: string,
cacheKey: string,
): Promise<void> {
const redisClient = this.cacheService.getRedisClient();
if (!redisClient) {
return;
}
const tagSetKey = this.getTagSetKey(namespace, tag);
const keyWithNamespace = this.cacheService.getNamespacedKey(
namespace,
cacheKey,
);
try {
await Promise.all([
redisClient.sadd(tagSetKey, keyWithNamespace),
redisClient.expire(tagSetKey, CACHE_TAG_SET_TTL_SECONDS),
]);
} catch (error) {
this.cacheService.recordError('tag remember', tagSetKey, error);
}
}
async invalidateTag(namespace: string, tag: string): Promise<void> {
const redisClient = this.cacheService.getRedisClient();
if (!redisClient) {
return;
}
const tagSetKey = this.getTagSetKey(namespace, tag);
try {
const keys = await redisClient.smembers(tagSetKey);
if (keys.length === 0) {
await redisClient.del(tagSetKey);
return;
}
const pipeline = redisClient.pipeline();
keys.forEach((key) => pipeline.del(key));
pipeline.del(tagSetKey);
await pipeline.exec();
} catch (error) {
this.cacheService.recordError('tag invalidate', tagSetKey, error);
}
}
private getTagSetKey(namespace: string, tag: string): string {
return this.cacheService.getNamespacedKey(
'cache-tag',
`${namespace}:${tag}`,
);
}
}

View File

@@ -1,12 +1,13 @@
import { Global, Module } from '@nestjs/common';
import { RedisModule } from '../../database/redis.module';
import { CacheTagsService } from './cache-tags.service';
import { CacheService } from './cache.service';
import { RedisThrottlerStorage } from './redis-throttler.storage';
@Global()
@Module({
imports: [RedisModule],
providers: [CacheService, RedisThrottlerStorage],
exports: [CacheService, RedisThrottlerStorage],
providers: [CacheService, CacheTagsService, RedisThrottlerStorage],
exports: [CacheService, CacheTagsService, RedisThrottlerStorage],
})
export class CacheModule {}