diff --git a/BE/src/game/redis/game-redis-memory.service.ts b/BE/src/game/redis/game-redis-memory.service.ts index d8669b7..416abf4 100644 --- a/BE/src/game/redis/game-redis-memory.service.ts +++ b/BE/src/game/redis/game-redis-memory.service.ts @@ -1,13 +1,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRedis } from '@nestjs-modules/ioredis'; import Redis from 'ioredis'; -import { Cron, CronExpression } from '@nestjs/schedule'; import { REDIS_KEY } from '../../common/constants/redis-key.constant'; +import { Cron, CronExpression } from '@nestjs/schedule'; @Injectable() export class GameRedisMemoryService { private readonly logger = new Logger(GameRedisMemoryService.name); private readonly BATCH_SIZE = 100; // 한 번에 처리할 배치 크기 + private readonly INACTIVE_THRESHOLD = 30 * 60 * 1000; // 30분 30 * 60 * 1000; private readonly TTL = { ROOM: 3 * 60 * 60, @@ -18,10 +19,60 @@ export class GameRedisMemoryService { constructor(@InjectRedis() private readonly redis: Redis) {} + /** + * 비활성 방 체크 (주기적으로 실행) + */ + /** + * 비활성 방을 체크하고 정리하는 크론 작업 + * SCAN을 사용하여 대규모 방 목록도 안전하게 처리 + */ + @Cron(CronExpression.EVERY_10_MINUTES) + async checkInactiveRooms(): Promise { + this.logger.verbose('비활성 방 체크 시작'); + try { + const now = Date.now(); + let cursor = '0'; + let processedCount = 0; + + do { + // SCAN을 사용하여 배치 단위로 처리 + const [nextCursor, rooms] = await this.redis.sscan( + REDIS_KEY.ACTIVE_ROOMS, + cursor, + 'COUNT', + this.BATCH_SIZE + ); + cursor = nextCursor; + + // 병렬로 방 상태 체크 및 처리 + await Promise.all( + rooms.map(async (roomId) => { + try { + const lastActivity = await this.redis.hget(REDIS_KEY.ROOM(roomId), 'lastActivityAt'); + + if (lastActivity && now - parseInt(lastActivity) > this.INACTIVE_THRESHOLD) { + await this.redis.publish('room:cleanup', roomId); + this.logger.verbose(`비활성으로 인해 방 ${roomId} 정리 시작`); + processedCount++; + } + } catch (error) { + this.logger.error(`방 ${roomId} 처리 중 오류 발생: ${error.message}`); + } + }) + ); + } while (cursor !== '0'); + + this.logger.verbose(`비활성 방 체크 완료: ${processedCount}개 방 정리됨`); + } catch (error) { + this.logger.error(`비활성 방 체크 중 오류 발생: ${error.message}`); + } + } + /** * TTL 관리를 위한 스케줄러 * 배치 처리로 블로킹 최소화 */ + // @Cron(CronExpression.EVERY_MINUTE) async manageTTL(): Promise { try { diff --git a/BE/src/game/redis/subscribers/room.cleanup.subscriber.ts b/BE/src/game/redis/subscribers/room.cleanup.subscriber.ts index be905cf..5409e85 100644 --- a/BE/src/game/redis/subscribers/room.cleanup.subscriber.ts +++ b/BE/src/game/redis/subscribers/room.cleanup.subscriber.ts @@ -37,6 +37,15 @@ export class RoomCleanupSubscriber extends RedisSubscriber { try { const pipeline = this.redis.pipeline(); + // 1. 방에 속한 플레이어 목록 가져오기, 200명미만 -> smembers 사용! + const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(roomId)); + + // 2. 플레이어 데이터 삭제 + for (const playerId of players) { + pipeline.del(REDIS_KEY.PLAYER(playerId)); // 플레이어 기본 데이터 + pipeline.del(`${REDIS_KEY.PLAYER(playerId)}:Changes`); // 플레이어 Changes 데이터 + } + // 1. 방 관련 기본 데이터 삭제 pipeline.del(REDIS_KEY.ROOM(roomId)); pipeline.del(REDIS_KEY.ROOM_PLAYERS(roomId)); diff --git a/BE/src/game/service/game.room.service.ts b/BE/src/game/service/game.room.service.ts index a44755a..da381e3 100644 --- a/BE/src/game/service/game.room.service.ts +++ b/BE/src/game/service/game.room.service.ts @@ -8,7 +8,6 @@ import { generateUniquePin } from '../../common/utils/utils'; import SocketEvents from '../../common/constants/socket-events'; import { UpdateRoomOptionDto } from '../dto/update-room-option.dto'; import { UpdateRoomQuizsetDto } from '../dto/update-room-quizset.dto'; -import { Cron, CronExpression } from '@nestjs/schedule'; import { Socket } from 'socket.io'; import { KickRoomDto } from '../dto/kick-room.dto'; import { TraceClass } from '../../common/interceptor/SocketEventLoggerInterceptor'; @@ -18,7 +17,6 @@ import { TraceClass } from '../../common/interceptor/SocketEventLoggerIntercepto export class GameRoomService { private readonly logger = new Logger(GameRoomService.name); private readonly INACTIVE_THRESHOLD = 30 * 60 * 1000; // 30분 30 * 60 * 1000; - // private readonly PLAYER_GRACE_PERIOD = 10; // 10초 constructor( @InjectRedis() private readonly redis: Redis, @@ -255,25 +253,6 @@ export class GameRoomService { await pipeline.exec(); } - /** - * 비활성 방 체크 (주기적으로 실행) - */ - @Cron(CronExpression.EVERY_MINUTE) - async checkInactiveRooms(): Promise { - const now = Date.now(); - const rooms = await this.redis.smembers(REDIS_KEY.ACTIVE_ROOMS); - this.logger.verbose(`비활성 방 체크시작 / 활성 방 목록: ${rooms}`); - - for (const roomId of rooms) { - const lastActivity = await this.redis.hget(REDIS_KEY.ROOM(roomId), 'lastActivityAt'); - - if (lastActivity && now - parseInt(lastActivity) > this.INACTIVE_THRESHOLD) { - await this.redis.publish('room:cleanup', roomId); - this.logger.verbose(`비활성으로 인해 방 ${roomId} 정리 시작`); - } - } - } - /** * 플레이어 관련 모든 데이터에 TTL 설정 */