- Spring Boot stomp-websocket & spring-boot-starter-data-redis 라이브러리를 활용하여 만든 채팅이 기반이 된 글입니다.
- WebSocket을 통해 들어오는 채팅 데이터 캐싱 처리할 것인지에 대해 설명합니다.
- DataBase : MySQL , Cache : Redis 로 활용한 방식을 설명합니다.
Write Back 방식의 데이터 흐름
1. Data Cache 저장
2. Cache에 있는 Data를 일정 기간동안 보관
3. 모여있는 Data를 DB에 저장
4. Cache에 있는 Data 삭제
public class ChatMessageSaveDto {
public enum MessageType{
ENTER,TALK,QUIT
}
private MessageType type;
private String roomId;
private String writer;
private String nickname;
private String message;
private String createdAt;
private List<String> userList;
}
@Service
@RequiredArgsConstructor
@Slf4j
public class ChatRedisCacheService {
private final RedisTemplate<String, ChatMessageSaveDto> chatRedisTemplate;
private final RedisTemplate<String, String> roomRedisTemplate;
// Redis의 Chatting data caching 처리
public void addChat(ChatMessageSaveDto chatMessageSaveDto) {
ChatMessageSaveDto savedData =
ChatMessageSaveDto.createChatMessageSaveDto(chatMessageSaveDto);
redisTemplate.opsForZSet()
.add(
NEW_CHAT,
savedData,
chatUtils.changeLocalDateTimeToDouble(savedData.getCreatedAt()));
}
//채팅 데이터 생성일자(String) -> Double 형으로 형변환
public Double changeLocalDateTimeToDouble(String createdAt) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS");
LocalDateTime localDateTime = LocalDateTime.parse(createdAt, formatter);
return ((Long) localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()).doubleValue();
}
@Component
@RequiredArgsConstructor
@Slf4j
public class ChatWriteBackScheduling {
private final RedisTemplate<String,Object> redisTemplate;
private final RedisTemplate<String, ChatMessageSaveDto> chatRedisTemplate;
private final ChatJdbcRepository chatJdbcRepository;
private final WorkSpaceRepository workSpaceRepository;
// 1시간 마다 Chatting data Redis -> MySQL
@Scheduled(cron = "0 0 0/1 * * *")
@Transactional
public void writeBack(){
log.info("Scheduling start");
BoundZSetOperations<String, ChatMessageSaveDto> setOperations
= chatRedisTemplate.boundZSetOps("NEW_CHAT");
ScanOptions scanOptions = ScanOptions.scanOptions().build();
List<Chat> chatList = new ArrayList<>();
try(Cursor<ZSetOperations.TypedTuple<ChatMessageSaveDto>> cursor
= setOperations.scan(scanOptions)){
while(cursor.hasNext()){
ZSetOperations.TypedTuple<ChatMessageSaveDto> chatMessageDto =cursor.next();
WorkSpace workSpace= workSpaceRepository
.findById(Long.parseLong(chatMessageDto.getValue().getRoomId()))
.orElse(null);
if(workSpace==null) {
continue;
}
chatList.add( Chat.of(chatMessageDto.getValue(),workSpace));
}
chatJdbcRepository.batchInsertRoomInventories(chatList);
redisTemplate.delete("NEW_CHAT");
}catch (Exception e){
e.printStackTrace();
}
log.info("Scheduling done");
}
}
Cache Aside
1. Redis에 해당하는 채팅 데이터가 있는지 확인
2. Redis에 해당하는 데이터가 없을 경우 DB에 추가조회 후,
3. 조회해 온 것을 다시 Redis에 올리는 방식
#cf ) 기존의 7일치 데이터를 매일 새벽 최신화 하는 방식을 추가로 사용
-Chatting data를 불러오기 위해 Cursor,채팅방 Id Frontend로 부터 받아온다.
public class ChatPagingDto {
private String message;
private String writer;
private String cursor;
}
public class ChatDataController {
private final ChatRedisCacheService cacheService;
@PostMapping("/api/chats/{workSpaceId}")
public ResponseDto<List<ChatPagingResponseDto>> getChatting(
@PathVariable Long workSpaceId,
@RequestBody(required = false) ChatPagingDto chatPagingDto){
//Cursor 존재하지 않을 경우,현재시간을 기준으로 paging
if(chatPagingDto==null||
chatPagingDto.getCursor()==null ||
chatPagingDto.getCursor().equals(""))
{
chatPagingDto= ChatPagingDto.builder()
.cursor( LocalDateTime
.now()
.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS")))
.build();
}
return cacheService.getChatsFromRedis(workSpaceId,chatPagingDto);
}
}
public ResponseDto<List<ChatPagingResponseDto>> getChatsFromRedis(Long workSpaceId,
ChatPagingDto chatPagingDto) {
//마지막 채팅을 기준으로 redis의 Sorted set에 몇번째 항목인지 파악
ChatMessageSaveDto cursorDto = ChatMessageSaveDto.builder()
.type(ChatMessageSaveDto.MessageType.TALK)
.roomId(workSpaceId.toString())
.createdAt(chatPagingDto.getCursor())
.message(chatPagingDto.getMessage())
.writer(chatPagingDto.getWriter())
.build();
//마지막 chat_data cursor Rank 조회
Long rank =
zSetOperations.reverseRank(CHAT_SORTED_SET_ + workSpaceId, cursorDto);
//Cursor 없을 경우 -> 최신채팅 조회
if (rank == null)
rank = 0L;
else rank = rank + 1;
//Redis 로부터 chat_data 조회
Set<ChatMessageSaveDto> chatMessageSaveDtoSet =
zSetOperations.reverseRange(CHAT_SORTED_SET_ + workSpaceId, rank, rank + 10);
List<ChatPagingResponseDto> chatMessageDtoList =
chatMessageSaveDtoSet
.stream()
.map(ChatPagingResponseDto::byChatMessageDto)
.collect(Collectors.toList());
//Chat_data 부족할경우 MYSQL 추가 조회
if (chatMessageDtoList.size() != 10) {
findOtherChatDataInMysql(chatMessageDtoList, workSpaceId, chatPagingDto.getCursor());
}
//redis caching 닉네임으로 작성자 삽입
for (ChatPagingResponseDto chatPagingResponseDto : chatMessageDtoList) {
chatPagingResponseDto
.setNickname(findUserNicknameByUsername(chatPagingResponseDto.getWriter()));
}
return ResponseDto.success(chatMessageDtoList);
}
private void findOtherChatDataInMysql(
List<ChatPagingResponseDto> chatMessageDtoList,
Long workSpaceId,
String cursor) {
String lastCursor;
// 데이터가 하나도 없을 경우 현재시간을 Cursor로 활용
if (chatMessageDtoList.size() == 0 && cursor == null) {
;
lastCursor = LocalDateTime
.now()
.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS"));
}
//redis 적재된 마지막 데이터를 입력했을 경우.
else if (chatMessageDtoList.size() == 0 && cursor != null) {
lastCursor = cursor;
}
// 데이터가 존재할 경우 CreatedAt을 Cursor로 사용
else lastCursor = chatMessageDtoList.get(chatMessageDtoList.size() - 1).getCreatedAt();
int dtoListSize = chatMessageDtoList.size();
Slice<Chat> chatSlice =
chatRepository
.findAllByCreatedAtBeforeAndWorkSpace_IdOrderByCreatedAtDesc(
lastCursor,
workSpaceId,
PageRequest.of(0, 30)
);
for (Chat chat : chatSlice.getContent()) {
cachingDBDataToRedis(chat);
}
//추가 데이터가 없을 때 return;
if (chatSlice.getContent().isEmpty())
return;
//추가 데이터가 존재하다면, responseDto에 데이터 추가.
for (int i = dtoListSize; i <= 10; i++) {
try {
Chat chat = chatSlice.getContent().get(i - dtoListSize);
chatMessageDtoList.add(ChatPagingResponseDto.of(chat));
} catch (IndexOutOfBoundsException e) {
return;
}
}
}
채팅 데이터 Scheduling 관련 코드링크
https://github.com/develkitProject/backend/tree/main/src/main/java/com/hanghae/final_project/global/util/scheduler
채팅 데이터 관련 로직 코드링크
https://github.com/develkitProject/backend/tree/main/src/main/java/com/hanghae/final_project/service/chat