Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUZ-83][FEATURE] 게임 랜덤 매칭 구현 #34

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from

Conversation

s0o0bn
Copy link
Collaborator

@s0o0bn s0o0bn commented Nov 27, 2024

✨ 구현 기능 명세

  • SSE 기반 랜덤 매칭 구현

✅ PR Point

전반적인 랜덤 매칭 플로우 및 알고리즘은 다음과 같이 구현했습니다.

  1. 브라우저에서 SSE 요청 시 Redis Queue, Set, Vector Hash 저장
  2. 새로운 매칭 요청이 들어왔다는 이벤트를 발행
  3. 해당 이벤트 핸들러에서 큐에 대기하고 있던 사용자 정보(레이팅, 관심 분야를 벡터화)를 꺼냄
  4. Redis에서 가져온 매칭 대기중인 사용자 벡터 중에서 기준 벡터에 대한 유사도 기준으로 5개 필터링
  5. 매칭된 사용자들이 5명이 채워지지 않았으면 현재 매칭 프로세스 종료, 됐으면 5명에게 각각 SSE 전송
  6. 매칭된 사용자들은 Redis에서 제거하고 브라우저는 SSE 연결 종료

위 로직에서 각각의 Redis 자료구조는 다음과 같은 용도로 사용됩니다.

  • Queue(List를 Queue처럼 사용): 매칭 대기 중인 사용자 저장 및 매칭 기준이 될 사용자 가져오기 위함
  • Set: 큐에서 가져온 사용자가 실제로 매칭 대기 중인지 확인(SSE 연결 유효 상태인지)하기 위함
    • 매칭이 성공할 때마다 각 사용자들을 큐에서 다 제거하고 나머지 사용자를 다시 큐에 집어넣는 방식은 비효율적일 것 같아 매칭이 되어도 일단 큐에 남겨두고 Set으로 유효성을 확인하고자 했습니다.
  • Hash: 게임 레이팅과 퀴즈 관심 카테고리를 기준으로 벡터화한 사용자 정보를 저장하고 벡터 유사도(코사인)을 계산하기 위함

지금은 레이팅 및 관심 분야를 테스트용으로 하드코딩한 로직으로 작성되어있는데,
각 서비스에서 API로 요청해서 가져오는 걸로 변경해야 합니다.

😭 어려웠던 점

지금 로직은 사용자 정보 벡터를 직접 Redis Hash로 저장하고, 매칭 후보를 찾을 때 전부 조회한 뒤 서버 상에서 필터링하고 있습니다.
이게 메모리 상에 부담이 될 거 같아 RediSearch 모듈을 통해 Redis 자체에서 KNN으로 조회하는 쿼리를 사용하려고 했는데,
오류가 너무 많이 나서 일단 보류하고 추후에 더 알아보고 리팩토링하려 합니다.

Copy link
Collaborator

@NaMinhyeok NaMinhyeok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

벡터랑 SSE가 저한텐 생소한 개념이라 궁금한거 질문 달았습니다.
Redis에서 벡터를 지원하는건가요 아니면 그냥 벡터DB처럼 사용 할 수 있게 한건가요..? 어렵네영


@GetMapping(
value = ["/subscribe"],
produces = [MediaType.TEXT_EVENT_STREAM_VALUE]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 sse 쓸 때 필요한건가요 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니당 저 옵션을 주면 연결 유지하면서 서버에서 이벤트 스트림으로 보내줄 수 있어요!

Comment on lines +13 to +16
return UserVector(
FloatArray(vectorSize).apply {
this[rating.index] = 1.0f
interests.forEach { this[it.index + GameRating.entries.size] = 1.0f }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 하면 vector db처럼 쓰는건가요..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저 로직 자체는 매칭할 사용자 정보를 벡터화해서 레디스에 저장하기 위해
레이팅이랑 관심 분야 값을 벡터로 전처리하는 로직입니당

데이터를 벡터로 변환하는 로직..? 이라고 보시면 될 거 같아용

Comment on lines +16 to +33
@Bean
fun userStatusTemplate(): RedisTemplate<String, RedisUserStatus> {
val redisTemplate = RedisTemplate<String, RedisUserStatus>()
redisTemplate.connectionFactory = redisConnectionFactory
redisTemplate.keySerializer = StringRedisSerializer()
redisTemplate.valueSerializer = Jackson2JsonRedisSerializer(RedisUserStatus::class.java)
return redisTemplate
}

@Bean
fun userIdTemplate(): RedisTemplate<String, String> {
val redisTemplate = RedisTemplate<String, String>()
redisTemplate.connectionFactory = redisConnectionFactory
redisTemplate.keySerializer = StringRedisSerializer()
redisTemplate.valueSerializer = StringRedisSerializer()
return redisTemplate
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 템플릿 각각 따로 지정하는 이유가 있나요 ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

첫 번째 템플릿은 data class를 redis 리스트에 추가하는 용이고
두 번째 템플릿은 그냥 userId 값을 set에 저장하려고 구분한거긴 합니다..!

딱히 이유가 있다기 보단 그냥 저장하는 데이터 타입이 달라서 나눠야하지 않을까 싶긴 했는데 안 나눠도 되나용?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 redisTemplate 하나 만들어두고 넣을 때 직렬화하는게 낫지않나요...?

어떤거는 Jackson으로 하고 어떤건 String으로 하시고 계속해서 객체가 생기면 Bean을 주입해줘야하지않나요? 객체마다 Config가 늘어나는거도 뭔가 좋지않은구조아닌가 싶어서..
저는 그래서 그냥 하나로 만들고 직렬화하고있어요

Comment on lines +33 to +34
val keyPattern = "$MATCHING_POOL_KEY:*"
val keys = redisTemplate.keys(keyPattern) ?: return emptyList()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 사실 잘 모르긴하는데 Redis 사용할 때 하지 말아야 될 1번이 keys 사용하기라고 하더라구요

keys 사용하는 대신 scan을 사용하는게 좋다고 하더라구요 싱글스레드기반이라 keys 사용시 모든 키를 가져오기 때문에 모든 요청이 밀릴수도 있다고 합니다...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니다....
말씀해주신거처럼 지금 구현된 로직은 메모리나 성능상 문제가 많아서
PR에 언급한 RediSearch를 사용해서 쿼리로 데이터를 가져오는 방법을 찾고 있어용

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 애초에 벡터DB가 아닌데 벡터DB처럼 쓰기 위해서 일단 전부 다 가져오고 비슷한걸 찾는 로직으로 구현하셔서 그런건가요 ??

레디스자체가 벡터를 지원을 안해서 그냥 Hash로 집어넣은 채로 꺼내서 이렇게 되는거죠..?

Copy link
Collaborator Author

@s0o0bn s0o0bn Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 일단 지금 구현은 그런 식이에요..!

레디스가 벡터를 지원하긴 합니다! 그런데 그러려면 RediSearch라는 모듈을 사용하고
서버에서도 Lettuce나 Jedis 같은 걸로 직접 쿼리문을 작성해서 사용하는 걸로 알고 있어용
약간 이런식입니당 Redis Vector Search

이거 PR 올리기 전에 시도 해봤는데 뭐가 잘 안돼서,,, 일단 보류했다가 뭔가 원인을 찾은 거 같아서 지금 다시 해보고 있습니당

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

레디스에 그냥 깡 레디스말고도 신기한게 되게 많네요.. 구현 잘되면 한번 저도 배워보겠습니다 ! 화이팅입니다

@s0o0bn
Copy link
Collaborator Author

s0o0bn commented Nov 27, 2024

지금은 그냥 순수 레디스에 벡터를 그냥 해시 형태로 저장해서 서버 단에서 처리하고 있어요
그런데 RediSearch 모듈을 사용하면 내부적으로 VectorDB를 지원한다고 합니다!

그래서 지금처럼 그냥 냅다 데이터 긁어와서 서버에서 필터링 하는 거 말고 레디스 쿼리로 KNN 같은 알고리즘으로 조회할 수 있다고 해용

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ feature 새로운 기능 구현 수빈
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants