-
Notifications
You must be signed in to change notification settings - Fork 0
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
refactor: 게임 엔진 서비스의 Busy Waiting 문제 개선 #83
The head ref may contain hidden characters: "refactor/#76-\uAC8C\uC784\uC5D4\uC9C4\uC11C\uBE44\uC2A4\uC758BusyWaiting\uBB38\uC81C\uAC1C\uC120"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ class QueueRepositoryAdapter( | |
|
||
override fun poll(roomId: Long, quizId: Long): AnswerResult? { | ||
val key = "roomId : " + roomId.toString() + ", " + "quizId : " + quizId.toString() | ||
return redisQueueRepository.poll(key)?.let { | ||
return redisQueueRepository.poll(key, 1)?.let { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분이 1초를 대기하고 poll한다는 의미일까요?🤗🤗 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1초동안 블럭킹으로 대기를하고, 없으면 null을 반환하도록 했습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네네 이해했습니다. 그리고 직접 게임 해보니까 10초가 참 짧게 느껴졌습니다. 대기시간 10초를 없애는 것도 같이 고려해봐도 좋을 것 같아요😀😀 |
||
objectMapper.readValue(it, AnswerResultEntity::class.java)?.toModel() | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package yjh.cstar.game.infrastructure.redis | |
|
||
import org.springframework.data.redis.core.RedisTemplate | ||
import org.springframework.stereotype.Repository | ||
import java.util.concurrent.TimeUnit | ||
|
||
@Repository | ||
class RedisQueueRepository( | ||
|
@@ -12,8 +13,8 @@ class RedisQueueRepository( | |
return redisTemplate.opsForList().rightPush(key, value) | ||
} | ||
|
||
fun poll(key: String): String? { | ||
return redisTemplate.opsForList().leftPop(key) | ||
fun poll(key: String, timeout: Long = 60, timeUnit: TimeUnit = TimeUnit.SECONDS): String? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. timeout, timeunit이 어떤 차이가 있을까요? 🤗🤗 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. timeout은 몇초동안 blocking을 할지 지정하는 파라미터이고, TimeUnit은 timout에 적어준 값의 기준값을 의미합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 친절하게 설명해주셔서 감사해요! 이해가 쏙쏙!! 되었습니다! |
||
return redisTemplate.opsForList().leftPop(key, timeout, timeUnit) | ||
} | ||
|
||
fun getSize(key: String): Long? { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package yjh.cstar.redis | ||
|
||
import org.junit.jupiter.api.AfterAll | ||
import org.junit.jupiter.api.BeforeAll | ||
import org.junit.jupiter.api.DisplayName | ||
import org.junit.jupiter.api.Test | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.test.context.SpringBootTest | ||
import org.springframework.test.context.ActiveProfiles | ||
import org.springframework.test.context.DynamicPropertyRegistry | ||
import org.springframework.test.context.DynamicPropertySource | ||
import org.testcontainers.containers.GenericContainer | ||
import org.testcontainers.utility.DockerImageName | ||
import yjh.cstar.game.application.GameAnswerQueueService | ||
import yjh.cstar.game.domain.AnswerResult | ||
import yjh.cstar.game.infrastructure.redis.RedisQueueRepository | ||
import java.util.Collections | ||
import java.util.concurrent.CountDownLatch | ||
import java.util.concurrent.Executors | ||
import kotlin.test.AfterTest | ||
import kotlin.test.BeforeTest | ||
import kotlin.test.assertEquals | ||
|
||
@ActiveProfiles("local-test") | ||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
@DisplayName("[Redis 동시성 테스트] GameAnswerQueueService") | ||
class RedisConcurrencyTest { | ||
|
||
@Autowired | ||
private lateinit var gameAnswerQueueService: GameAnswerQueueService | ||
|
||
@Autowired | ||
private lateinit var redisQueueRepository: RedisQueueRepository | ||
|
||
companion object { | ||
private const val ROOM_ID = 1L | ||
private const val QUIZ_ID = 1L | ||
private const val KEY = "roomId : " + ROOM_ID + ", " + "quizId : " + QUIZ_ID | ||
|
||
private val redis: GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:latest")) | ||
.withExposedPorts(6379) | ||
.withReuse(true) | ||
Comment on lines
+40
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서 GenericContainer는 어떤 역할을 하는지 여쭤봐도 될까요?😆😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 구문은 컨테이너를 활용하기 위해 사용하였는데요, 특정 버전의 redis 이미지를 가져오고 컨테이너화 시킬 때 사용되는 클래스입니다! 😀😀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컨테이너화... 오 좋은 구문이 많네요! 이렇게 또 배워갑니다!😀😀 |
||
|
||
@JvmStatic | ||
@DynamicPropertySource | ||
fun properties(registry: DynamicPropertyRegistry) { | ||
registry.add("spring.data.redis.host", redis::getHost) | ||
registry.add("spring.data.redis.port", redis::getFirstMappedPort) | ||
} | ||
|
||
@BeforeAll | ||
@JvmStatic | ||
fun beforeAll() { | ||
redis.start() | ||
} | ||
|
||
@AfterAll | ||
@JvmStatic | ||
fun afterAll() { | ||
redis.stop() | ||
} | ||
} | ||
|
||
@BeforeTest | ||
fun beforeEach() { | ||
redisQueueRepository.deleteAll(KEY) | ||
} | ||
|
||
@AfterTest | ||
fun afterEach() { | ||
redisQueueRepository.deleteAll(KEY) | ||
} | ||
|
||
@Test | ||
fun `레디스 사용자 퀴즈 정답 큐 동시성 테스트`() { | ||
// given | ||
val numberOfThreads = 2 | ||
val startLatch = CountDownLatch(1) | ||
val doneLatch = CountDownLatch(numberOfThreads) | ||
val executor = Executors.newFixedThreadPool(numberOfThreads) | ||
val receive = Collections.synchronizedList(mutableListOf<AnswerResult>()) | ||
|
||
val answerResult = AnswerResult( | ||
answer = "정답", | ||
quizId = QUIZ_ID, | ||
roomId = ROOM_ID, | ||
playerId = 1, | ||
nickname = "nickname" | ||
) | ||
|
||
// when | ||
executor.submit { | ||
try { | ||
for (idx in 1..100) { | ||
gameAnswerQueueService.add(answerResult) | ||
} | ||
} catch (e: Exception) { | ||
println("Error: ${e.message}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 편의를 위해서 print문을 쓰신 걸까요? 그렇다면 남겨두는 것도 좋을 것 같습니다. 😁😁 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 편의를 위해서 사용하였는데, 혹시 나중에 성능적으로 문제가 될 것 같으면 로그를 찍는 방식으로 수정해봐도 좋을 것 같아요!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네네 알겠습니다. 나중에 print문이 있는 파트를 다 찾아서 로그로 바꿔봐도 좋을 것 같아요! |
||
} finally { | ||
doneLatch.countDown() | ||
} | ||
} | ||
|
||
executor.submit { | ||
try { | ||
for (idx in 1..100) { | ||
receive.add(gameAnswerQueueService.poll(ROOM_ID, QUIZ_ID)) | ||
} | ||
} catch (e: Exception) { | ||
println("Error: ${e.message}") | ||
} finally { | ||
doneLatch.countDown() | ||
} | ||
} | ||
|
||
startLatch.countDown() // 모든 스레드 동시에 시작 | ||
|
||
doneLatch.await() // 모든 스레드 종료 대기 | ||
|
||
executor.shutdown() | ||
|
||
// then | ||
val pollSize = receive.filter { it != null } | ||
.size | ||
val remainPushSize = redisQueueRepository.getSize(KEY) ?: 0 | ||
|
||
assertEquals(100, pollSize + remainPushSize) | ||
} | ||
|
||
@Test | ||
fun `레디스 List 삽입 동시성 테스트`() { | ||
// given | ||
val numberOfThreads = 100 | ||
val startLatch = CountDownLatch(1) | ||
val doneLatch = CountDownLatch(numberOfThreads) | ||
val executor = Executors.newFixedThreadPool(numberOfThreads) | ||
|
||
val answerResult = AnswerResult( | ||
answer = "정답", | ||
quizId = QUIZ_ID, | ||
roomId = ROOM_ID, | ||
playerId = 1, | ||
nickname = "nickname" | ||
) | ||
|
||
// when | ||
// 데이터 추가 | ||
for (idx in 1..numberOfThreads) { | ||
executor.submit { | ||
try { | ||
gameAnswerQueueService.add(answerResult) | ||
} catch (e: Exception) { | ||
println("Error: ${e.message}") | ||
} finally { | ||
doneLatch.countDown() | ||
} | ||
} | ||
} | ||
|
||
startLatch.countDown() // 모든 스레드 동시에 시작 | ||
|
||
doneLatch.await() // 모든 스레드 종료 대기 | ||
|
||
executor.shutdown() | ||
|
||
// then | ||
val remainPushSize = redisQueueRepository.getSize(KEY) ?: 0 | ||
|
||
assertEquals(100, remainPushSize) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만약 key가 여러 곳에서 쓰인다면 분리해보는 것도 좋을 것 같습니다.😀😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞아요 레디스에서 다양한 키를 사용할 수 있고, 키 명명 규칙도 존재하기 때문에 별도의 파일에서 관리해주면 좋을 것 같습니다!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다! 이렇게 이슈가 늘어나는 거죠! ㅎㅎ 여기저기 쓰이면서 명명 규칙이 혼재해서 사용될 수 있기 때문에 분리해서 관리하는 게 좋을 것 같습니다.