Nest.jsにてリクエストで共通のキャッシュを持つ
Nest.js
速度改善
速度改善のためクエリの調査を行なっていたところ、割と重いデータ取得を1リクエスト内で重複して取得していた。 redisなどでcacheするのも良いが、導入にはそれなりのコストが発生するため、Nest.jsのInterceptorとasyncLocalStorageを使ってリクエスト毎にキャッシュを持つようにした。 要件によっては逆にcacheが悪さする可能性があるため、運用は要注意。
import { AsyncLocalStorage } from 'async_hooks';
export class RequestCacheStore {
private static asyncLocalStorage = new AsyncLocalStorage<
Map<string, unknown>
>();
static getStore(): Map<string, unknown> | null {
const store = this.asyncLocalStorage.getStore();
// RequestCacheInterceptorを使っておらず、cacheを使用とした場合はnullを返す
if (!store) {
return null;
}
return store;
}
static run<T>(callback: () => Promise<T>): Promise<T> {
return this.asyncLocalStorage.run(new Map(), callback);
}
}
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { firstValueFrom } from 'rxjs';
import { RequestCacheStore } from '../cache/RequestCacheStore';
@Injectable()
export class RequestCacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return new Observable((subscriber) => {
RequestCacheStore.run(async () => {
try {
const result = await firstValueFrom(next.handle());
subscriber.next(result);
subscriber.complete();
} catch (error) {
subscriber.error(error);
}
});
});
}
}
上記をcontrollerで指定
@Controller('users') @UseInterceptors(RequestCacheInterceptor) export class UserController { ~ 略 ~
cacheServiceを定義
@Injectable()
export class UserCacheService {
get(
userSession: UserSession,
): UserEntity | null {
const store = RequestCacheStore.getStore();
if (!store) {
return null;
}
const key = this.getCacheKey(userSession);
return (
(store.get(key) as UserEntity) ||
null
);
}
set(
userSession: UserSession,
data: UserEntity,
): void {
const store = RequestCacheStore.getStore();
if (!store) {
return;
}
const key = this.getCacheKey(userSession);
store.set(key, data);
}
private getCacheKey(
userSession: UserSession,
): string {
return `user:${userSession.organizationId}:${userSession.userId}`;
}
}
service等でcacheを見るように追加する
async findById( userSession: UserSession, ): Promise<UserEntity> { const cache = this.userCacheService.get(userSession); if (cache) { return cache as ObjectConfigEntity; } const user = await this.fetchUser(userSession); this.userCacheService.set(userSession, user); return user; }