Nest.jsにてリクエストで共通のキャッシュを持つ
Nest.js
速度改善
バックエンド
知識
運用
速度改善のためクエリの調査を行なっていたところ、割と重いデータ取得を1リクエスト内で重複して取得していた。 redisなどでcacheするのも良いが、導入にはそれなりのコストが発生するため、Nest.jsのInterceptorとasyncLocalStorageを使ってリクエスト毎にキャッシュを持つようにした。 要件によっては逆にcacheが悪さする可能性があるため、運用は要注意。
ts
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);
}
}ts
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で指定
ts
@Controller('users')
@UseInterceptors(RequestCacheInterceptor)
export class UserController {
~ 略 ~cacheServiceを定義
ts
@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を見るように追加する
ts
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;
}