실시간 매칭(Redis)

Sol's·2023년 2월 7일
1

팀프로젝트

목록 보기
23/25
post-thumbnail


지금 진행하는 팀 프로젝트는 운동을 같이할 운동메이트를 동네에서 찾는게 핵심입니다.

여기에 추가로 실시간 매칭기능을 넣으려고 하고 있습니다.
원래는 운동 종목별로, 가까운 사람들 기준으로 등등 여러가지 사항을 고려해야하지만
프로젝트 제출 시간이 얼마 없기에 가능한 만큼 실시간 매칭기능을 만들어보려고 합니다.

로직 흐름

  1. 실시간 매칭을 누르고 종목을 선택하면
  2. 종목에 맞는 대기열에 들어가고
  3. 인원이 채워진다면 (3명이 모인다면)
  4. 모임방이 만들어지고, 채팅방으로 이동이 됩니다.

실시간 매칭 흐름도

Redis의 Sorted Set

Redis는 싱글 스레드이기에 실시간 매칭을 할때 동시성문제가 발생하지 않을것이라 판단하여 Redis를 적용하기로 했습니다.

처음에는 List를 사용해서 대기열을 만들려고 했습니다.
하지만 List는 중복체크기능이 되지 않았습니다...

순서를 보장하기위해 Queue를 그리고 중복을 잡기위해 Set을 사용해야하는데 좋은게 없을까? 라고생각하다
Redis의 Sorted Set을 알게되었습니다.
중복을 제거해주고, 순서도 보장할 수 있으니 실시간 매칭 대기열에 딱이였습니다!

순서를 보장하기위해 Sorted Set에 현재 시간을 넣었습니다.

실시간 매칭 시작하기

카테고리 입력받기

<div class="boxTest btn-random gap-3 d-md-flex justify-content-md-end">
                        <p><div class="btn-random-text" id="randomCnt" sec:authorize="isAuthenticated()"  th:text="${randomCnt}">현제 대기중인 인원 : </div></p>
                        <button class="btn btn-danger" style="display: none" type="button" id="randomMatchCancel_btn"
                                onclick="randomMatchCancel()">
                            + 실시간 매칭 취소🌌
                        </button>
                        <button class="btn btn-outline-danger" sec:authorize="isAuthenticated()" type="button" id="randomMatch_btn" onclick="randomMatch()">
                            + 실시간 매칭🔥
                        </button>
                    </div>

실시간 매칭 버튼을 누르면 randomMatch() js의 함수가 실행되어 운동 카테고리를 선택하고 대기열로 입장합니다.
간단히 alert 창으로 카테고리를 입력받았습니다.

대기열 입장하기

//실제 매칭을 잡는 로직
function startMatching(level, sport) {
    let username = document.getElementById("myName").innerText;
    $.ajax({
        type: "POST",
        url: '/api/v1/match/live' + "?username=" + username + "&sport=" + sport,
        success: function (data) {
            let listCnt = data;
            if (listCnt > 0) {
                Swal.fire({
                    icon: 'success',
                    title: '실시간 매칭이 시작되었습니다🔥\n\n Level : '+ level,
                    html:  '매칭이 성사될때까지 대기해 주세요👍<br> 3명이 대기열에 들어오면 매칭됩니다',
                });
                let randomMatchCancelBtn = document.getElementById("randomMatchCancel_btn");
                let randomMatchBtn = document.getElementById("randomMatch_btn");
                randomMatchCancelBtn.style.display = 'block';
                randomMatchBtn.style.display = 'none';
            }
        },
        error: function (request, status, error) {
            alert("로그인 후 랜덤매칭이 가능합니다.")
        }
    });
}

위에서 카테고리를 선택했으면 startMatching() 함수가 id와 입력받은 운동 카테고리를 갖고 ajax 통신을 하게됩니다.
url: '/api/v1/match/live' + "?username=" + username + "&sport=" + sport,

매칭 로직

컨트롤러 로직

	@PostMapping("/live")
    @Transactional
    public int randomMatch(@RequestParam String username, @RequestParam String sport) {
        return liveMatchService.randomMatch(username, sport);
    }

username과 sport를 파라미터로 받아 service딴으로 넘겨줍니다.

서비스 로직

이해를 위해 서비스 로직을 나누어 설명하겠습니다!

  • 대기열 입장
 public int randomMatch(String username, String sport) {

        // redis에 대기열 순서대로 삽입, 현재 시간을 score로 잡음
        redisTemplate.opsForZSet().add(sport, username, System.currentTimeMillis());

        // 현재 대기열의 총 숫자 확인
        Long randomMatchListInRedis = redisTemplate.opsForZSet().zCard(sport);

        //대기인원을 sport 종목에 따라 UI에 뿌려주는 로직
        sendSportListCntToUser(sport); 

랜덤매칭을 누르고 대기열에 들어온 상태입니다.
이제 랜덤매칭 List에 3명이상이 되면 방을 생성하고 채팅방을 생성해서 채팅방으로 이동시키면 됩니다

  • 크루 생성 및 채팅방 생성
        if (randomMatchListInRedis >= 3) {
            String[] UserListInRedis = new String[4];
            Set<String> range = redisTemplate.opsForZSet().range(sport, 0, 3);

            Iterator<String> iterator = range.iterator();
            int listCnt = 0;
            while (iterator.hasNext()) {
                String next = iterator.next();
                UserListInRedis[listCnt] = next;
                listCnt++;
            }

            User fistUser = findUserFromRedis(UserListInRedis[0]);
            User secondUser = findUserFromRedis(UserListInRedis[1]);
            User thirdUser = findUserFromRedis(UserListInRedis[2]);


            Crew savedLiveMatchCrew = makeCrew(fistUser, secondUser, thirdUser, sport);
            makeParticipationsAndUpdateToCrew(fistUser, secondUser, thirdUser, savedLiveMatchCrew, sport);
            deleteRedisLiveMatchLists(UserListInRedis, sport);
            sendSseToUsers(fistUser, secondUser, thirdUser, savedLiveMatchCrew);
        }
        return 1;
    }
  1. 대기열의 User들을 담기위한 UserListInRedis 배열을 생성합니다.
  2. 대기열에서 User목록 3명을 꺼내 UserListInRedis 배열에 넣습니다.
  3. UserListInRedis에 있는 User명단을 통해 DB에서 User를 찾습니다.
  4. 찾은 User정보로 방을 생성합니다 -> makeCrew
  5. User정보를 통해 Participation을 만들고 Crew테이블에 저장합니다.
    (Crew의 참여했다는 것을 Particitation의 정보로 확인하기때문입니다.)
  6. 매칭이 잡힌 User중 대기열에 있는 User들을 삭제합니다.
  7. 알림을 발송합니다.

다음 블로그

  1. 랜덤매칭을 취소할 경우 어떻게 처리할까?
  2. 동시성에 대해 잘못 생각한 점 -> 모든 로직이 Redis에 공유되어야 할텐데?
  3. 카테고리별로 랜덤매칭을 한다면 어떻게? -> 저장 시 카테고리_username으로 구분
  4. 로그아웃하면? -> 로그아웃을 하더라도 매칭이 잡히게 하였습니다.

참고자료

Redis란? 레디스의 기본적인 개념 (인메모리 데이터 구조 저장소)
파티매칭의 구현
[요청강좌]온라인게임제작강좌 part6. 랜덤매칭시스템 강좌
Redis Sorted Set
레디스 List, Sorted Set 자료형

profile
배우고, 생각하고, 행동해라

0개의 댓글

관련 채용 정보