Redis 기반 채팅 리스트 읽음/읽지 않음 알림 시스템
Redis로 채팅 리스트의 읽음/읽지 않음 상태를 실시간 관리.
채팅서비스는 메세지의 읽음과 읽지 않음 상태가 표시되는 것이 일반적이다. 그래서 개발하려고 공부하던중 이것또한 하나씩 데이터 베이스에 변경사항이 있을 때 마다 들어간다면 부하를 일으킬 것 같았다
그래서 앞에서 메세지 저장에 사용한 것 처럼 Redis를 써보기로 했다.
-
- 채팅 리스트에서 각 채팅방의 읽은/읽지 않은 메시지를 실시간으로 표시.
- 메시지량 증가 시 MySQL 과부하 방지 및 빠른 상태 업데이트를 위해 Redis 활용.
목표
- 상태 변수:
-
isActive
플래그로 유저의 활성 상태 관리.
- 이벤트 감지:
-
- 채팅방 입장: 처음 입장 시 활성 상태 설정.
- 창 닫기/탭 전환:
visibilitychange
,blur
,focus
,beforeunload
이벤트로 비활성/활성 전환. - 기타: 앱 클릭, 브라우저 최소화, 페이지 이동 등 감지.
document.addEventListener("visibilitychange", () => { document.hidden ? setUserInactive() : setUserActive(); }); window.addEventListener("blur", setUserInactive); window.addEventListener("focus", setUserActive); window.addEventListener("beforeunload", setUserInactive); }
- WebSocket 전송:
-
- 활성 시:
/app/user-active
로chatRoomId
,userId
전송.
function setUserActive() { if (isActive) return; isActive = true; stompClient.publish({ destination: "/app/user-active", body: JSON.stringify({ "chatRoomId": chatRoomId, "userId": userId }) }); }
- 비활성 시:
/app/user-inactive
로 동일 데이터 전송.function setUserInactive() { if (!isActive) return; isActive = false; stompClient.publish({ destination: "/app/user-inactive", body: JSON.stringify({ "chatRoomId": chatRoomId, "userId": userId }) }); }
- 활성 시:
프론트엔드 유저 액티비티 트래킹
- 키 구조:
-
"lastReadTimer:
→: " ChatRoomUserDTO
로 유저의 마지막 읽음 타임스탬프 저장.
- 동작:
-
- 유저 활성화:
/app/user-active
수신 → Redis에ChatRoomUserDTO
저장 (addPendingLastReadAt
), 현재 타임스탬프 갱신. - 유저 비활성화:
/app/user-inactive
수신 → Redis에 타임스탬프 갱신.
public void addPendingLastReadAt(ChatRoomUserDTO chatRoomUserDto) { String key = "lastReadTimer:" + chatRoomUserDto.getUserId() + ":" + chatRoomUserDto.getChatRoomId(); chatRoomUserRedisTemplate.opsForValue().set(key, chatRoomUserDto); log.info("마지막 접속 시간: {}, 유저id: {}, 챗id: {}", chatRoomUserDto.getLastReadAt(), chatRoomUserDto.getUserId(), chatRoomUserDto.getChatRoomId()); }
- 읽음 기준:
ChatRoomUserDTO.lastReadAt
과 메시지의enrolledAt
비교로 읽음/읽지 않음 판단. - 유저 활성화:
- 변경점:
-
- 기존 단순 타임스탬프 대신
ChatRoomUserDTO
로 데이터 구조화.
- 기존 단순 타임스탬프 대신
Redis로 실시간 상태 관리
- 세션 종료 시:
-
SessionDisconnectEvent
발생 → Redis의"lastReadTimer:
데이터를 MySQL에 반영 후 삭제 (:*" removePendingLastReadAt
).
- 활성 데이터 조회:
-
SimpMessageHeaderAccessor
로sessionId
에서userId
추출, Redis 데이터 매핑.
MySQL 동기화 및 정리
- 유저 입장:
-
- 채팅 히스토리 로드 → Redis에
lastReadAt
업데이트 (addPendingLastReadAt
) → 활성 상태 표시.
- 채팅 히스토리 로드 → Redis에
- 유저 활성화:
-
/app/user-active
→ Redis에ChatRoomUserDTO
저장 → 읽음 상태 갱신.
- 유저 비활성화:
-
/app/user-inactive
→ Redis에 타임스탬프 갱신 → 이후 메시지 읽지 않음 처리.
- 유저 disconnection:
-
- WebSocket 이벤트 감지 → Redis 데이터 MySQL 동기화 → Redis 정리 (
removePendingLastReadAt
).
- WebSocket 이벤트 감지 → Redis 데이터 MySQL 동기화 → Redis 정리 (
동작 방식
- 문제:
-
- 신규 채팅방 생성 시 Redis에 관계 미등록 (
"user_chatrooms:
," "chatroom_users:
누락)."
- 신규 채팅방 생성 시 Redis에 관계 미등록 (
- 해결:
-
- 생성 시
addChatRoomIdsAndUserIds(userId, chatRoomId)
호출 → Redis 즉시 업데이트.
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); } - 생성 시
- 결과:
-
- 신규 채팅방의 읽음 상태도 정상 반영.
신규 채팅방 문제 해결
- 문제 해결:
-
- 실시간 읽음/읽지 않음 표시, MySQL 과부하 방지, 신규 채팅방 지원.
- Redis:
-
"lastReadTimer:
로: " ChatRoomUserDTO
저장, 빠른 상태 관리.
- 프론트엔드:
-
- 액티비티 이벤트로 활성/비활성 트래킹, WebSocket 전송.
- MySQL:
-
- 세션 종료 시 Redis 데이터 동기화로 영구 저장.
- 결과:
-
- Redis로 성능 최적화, 모든 채팅방에서 실시간 읽음 상태 반영.
요약
Posted on 2025.03.01