Redis 기반 채팅 메시지 저장 및 성능 개선
Redis를 활용해 채팅 메시지 저장 성능을 개선하고 MySQL 과부하를 방지.
채팅 서비스를 개발하면서 지금은 작은 프로젝트이지만 만약에 많은 사람들이 이용하게 되는 서비스라고 가정했을 때, 지금처럼 모든 메세지들이 하나씩 계속 데이터베이스에 들어가는 건 데이터 베이스에 과부하를 줄 것 같다는 생각이 들었다.
어떻게하면 데이터베이스의 과부하를 조금 예방할 수 있을까 고민하다가 Redis를 도입하기로했다
메세지를 서버가 받았을 때 바로 데이터 베이스에 넣는 것이 아닌, Redis에 잠깐 저장해뒀다가 한 번에 저장하는 것이다.
- 문제:
-
- 메시지가 생성될 때마다 MySQL에 즉시 저장, 메시지량 증가 시 빈번한 DB 쓰기로 과부하 발생 가능.
- 목표:
-
- 실시간 처리 유지하면서 DB 부담 줄이기.
기존 문제점: MySQL 과부하 우려
- Redis 버퍼링:
-
- 새 메시지를 Redis에 저장 (키:
"message_queue:
) → MySQL 쓰기 지연."
public void addPendingMessage(MessageDTO messageDTO) throws Exception { String key = "message_queue:" + messageDTO.getChatRoomId(); redisTemplate.opsForList().rightPush(key, messageDTO); // Add to end //toggle refresh to all users of that chatroom that is in the chat list notificationService.toggleRefresh(getUserIdsByChatRoomId(messageDTO.getChatRoomId())); }
- 새 메시지를 Redis에 저장 (키:
- 빠른 처리:
-
- Redis의 인메모리 특성으로 메시지 저장 속도 향상, 실시간성 확보.
- 추가 활용:
-
- 메시지뿐만 아니라 유저-채팅방 관계 캐싱 (키:
"user_chatrooms:
," "chatroom_users:
)."
//두가지로 나눈 이유는 유저의 입장, 채팅방의 입장. 따로 저장해야 찾기가 쉬워짐. 쿼리가 그나마 덜 복잡해짐 -> 성능 개선 예상. public void addChatRoomIdsAndUserIds(Integer userId, Long chatRoomId) { String keyForUserId = "user_chatrooms:" + userId; Set
chatRoomIds = chatRoomIdRedisTemplate.opsForSet().members(keyForUserId); //레디스에 없으면 디비에서 불러오기.(expired 되어서 없어짐) if (chatRoomIds == null || chatRoomIds.isEmpty()) { List dbChatRoomIds = chatRoomUserRepository.findChatRoomIdsByUserId(userId); chatRoomIdRedisTemplate.opsForSet().add(keyForUserId, dbChatRoomIds.toArray(new Long[0])); chatRoomIdRedisTemplate.expire(keyForUserId, 24 * 60 * 60, TimeUnit.SECONDS); // 24 hours TTL log.info("Cached {} chat room IDs for user {} in Redis with 24-hour expiry", dbChatRoomIds.size(), userId); } // Add chatRoomId to user's set chatRoomIdRedisTemplate.opsForSet().add(keyForUserId, chatRoomId); chatRoomIdRedisTemplate.expire(keyForUserId, 24 * 60 * 60, TimeUnit.SECONDS); // 24 hours TTL log.info("Added chatRoomId {} to user {} in Redis with 24-hour expiry", chatRoomId, userId); String keyForChatRoomId = "chatroom_users:" + chatRoomId; Set userIds = userIdRedisTemplate.opsForSet().members(keyForChatRoomId); //레디스에 없으면 디비에서 불러오기.(expired 되어서 없어짐) if (userIds == null || userIds.isEmpty()) { List dbUserIds = chatRoomUserRepository.findUserIdsByChatRoomId(chatRoomId); userIdRedisTemplate.opsForSet().add(keyForChatRoomId, dbUserIds.toArray(new Integer[0])); userIdRedisTemplate.expire(keyForChatRoomId, 24 * 60 * 60, TimeUnit.SECONDS); // 24 hours TTL log.info("Cached {} user IDs for chat room {} in Redis with 24-hour expiry", dbUserIds.size(), chatRoomId); } // Add userId to chat room's set userIdRedisTemplate.opsForSet().add(keyForChatRoomId, userId); userIdRedisTemplate.expire(keyForChatRoomId, 24 * 60 * 60, TimeUnit.SECONDS); // 24 hours TTL log.info("Added userId {} to chatRoomId {} in Redis with 24-hour expiry", userId, chatRoomId); } - 메시지뿐만 아니라 유저-채팅방 관계 캐싱 (키:
Redis 도입으로 성능 개선
- 주기적 동기화:
-
@Scheduled
로 30분마다 Redis의"message_queue:
메시지를 MySQL에 배치 저장."
@Transactional @Scheduled(fixedRate = 1800000) // 30 minutes public void syncAllMessages() { List
pendingMessages = redisService.getAllPendingMessages(); if (pendingMessages.isEmpty()) { log.info("스케쥴러 실행. 전체 싱크할 메세지 없음"); return; } List messages = pendingMessages.stream() .map(dto -> { Message message = new Message(); message.setChatRoom(chatRoomService.getChatRoomById(dto.getChatRoomId())); // Handle system messages (senderId = 0 or null) if (dto.getSenderId() == null || dto.getSenderId() == 0) { message.setUser(null); // System message } else { message.setUser(userService.getUserById(dto.getSenderId())); } message.setContent(dto.getContent()); message.setEnrolledAt(dto.getEnrolledAt()); return message; }) .toList(); messageRepository.saveAll(messages); log.info("스캐줄러 Sync all messages 레디스 전체 메세지 싱크 완료"); redisService.removeAllPendingMessages(); } - 배치 처리:
-
- 개별 메시지마다 DB 쓰기 대신 일정 간격으로 묶어 저장 → 쓰기 요청 횟수 감소.
- 삭제:
-
- MySQL 저장 후 해당
chatRoomId
의 Redis 큐 삭제 (removePendingMessage
).
- MySQL 저장 후 해당
스케줄링으로 MySQL 동기화
- 메시지 전송:
-
- 클라이언트가 메시지 전송 →
MessageDto
를 Redis에 저장 (addPendingMessage
). chatRoomId
의 유저 목록 조회 ("chatroom_users:
) → WebSocket으로 실시간 알림."
- 클라이언트가 메시지 전송 →
- 동기화:
-
- 10초마다 Redis의 pending 메시지를 MySQL에 저장 (
syncMessagesByUserId
등). - 저장 후 Redis에서 삭제 (
removePendingMessage
).
- 10초마다 Redis의 pending 메시지를 MySQL에 저장 (
- 채팅 로드:
-
- Redis 체크 후 pending 메시지 있으면 MySQL 동기화, 이후 MySQL에서 전체 메시지 조회.
- 채팅 목록:
-
- 유저 입장 시
getChatRoomIdsByUserId
로"user_chatrooms:
조회 → 신규 채팅방 포함."
- 유저 입장 시
동작 방식
- 문제:
-
- 신규 채팅방 생성 시 Redis에 등록 안 됨 →
getChatRoomIdsByUserId
,getUserIdsByChatRoomId
에서 누락.
- 신규 채팅방 생성 시 Redis에 등록 안 됨 →
- 해결:
-
- 생성 시
addChatRoomIdsAndUserIds(userId, chatRoomId)
호출 →"user_chatrooms:
와" "chatroom_users:
즉시 업데이트."
- 생성 시
- 결과:
-
- 신규 채팅방도 Redis에 반영, 정상 조회 및 알림 동작.
신규 채팅방 문제 해결
- 문제 해결:
-
- 메시지량 증가로 인한 MySQL 과부하 방지, 신규 채팅방 캐싱 문제 해결.
- Redis:
-
"message_queue:
로 메시지 버퍼링, DB 쓰기 지연." "user_chatrooms:
," "chatroom_users:
로 관계 캐싱."
- 스케줄링:
-
- 10초 주기 배치 동기화로 쓰기 부하 감소.
- 결과:
-
- 실시간 메시지 처리, 알림, 채팅 목록 업데이트와 DB 성능 최적화를 모두 달성.
요약
Posted on 2025.03.01