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

[feat] 선착순 이벤트 구현방안 2: Redis SortedSet + Lua 기반 (#29) #30

Merged
merged 7 commits into from
Aug 6, 2024

Conversation

win-luck
Copy link
Collaborator

@win-luck win-luck commented Aug 5, 2024

#️⃣ 연관 이슈

ex) #29

📝 작업 내용

사전 조사 결과를 바탕으로 SortedSet 기반의 선착순 이벤트를 구현하고, 기존 Redis Lock 코드를 리팩토링했습니다.
구체적인 설명은 코멘트에 포함하겠습니다.

  • 기존의 Redis Lock 코드 리팩토링하여 모든 구현방안에서 동일한 자료구조 사용하도록 일관성 개선
  • SortedSet 기반 구현
  • 부하 테스트 작성

참고 이미지 및 자료

💬 리뷰 요구사항

@win-luck win-luck added the feat 기능 구현 label Aug 5, 2024
@win-luck win-luck requested a review from blaxsior August 5, 2024 08:59
@win-luck win-luck linked an issue Aug 5, 2024 that may be closed by this pull request
3 tasks
Copy link
Collaborator Author

@win-luck win-luck left a comment

Choose a reason for hiding this comment

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

현재 테스트코드가 갑자기 에러가 나서, 해결 및 리뷰 마치신 후 머지하도록 하겠습니다.

Comment on lines +48 to +66
// 선착순 이벤트의 종료 여부 flag를 저장하기 위해 사용
@Bean
public RedisTemplate<String, Boolean> booleanRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Boolean> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}

// 선착순 이벤트의 참여 가능 인원수를 저장하기 위해 사용
@Bean
public RedisTemplate<String, Integer> numberRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Integer> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

기존 Lock 방식이 RedissonClient를 참고하다가 RedisTemplate<>를 사용하는 등 난해하여 유지보수에 어려움이 있었습니다. 다음과 같은 방안을 검토했고, 2번을 채택했습니다.

1. 선착순 이벤트 관련 정보를 "객체"로 저장하는 방안

이 경우 편리한 정보 관리가 가능했지만, 잔여 티켓 수를 조정하는 과정에서 동시성 이슈가 지속적으로 발생했습니다.

2. 선착순 이벤트 관련 정보를 분리하여 각각 저장하는 방안

참여 가능 인원수는 Long, 종료 여부는 Boolean으로 저장해두어서, 이벤트가 이미 종료되었는데 비즈니스 로직 안쪽으로 들어오는 경우를 방지하고, 참여 가능 인원수를 미리 메모리에 저장함으로써 성능을 높이고자 했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

이벤트 관련 정보도 redis에 담으면 사용자 요청마다 db에 접근할 필요가 없다는 점이 좋은 것 같습니다.

Comment on lines +38 to 42
events.forEach(event -> { // 당첨자 수, 마감 여부, 시작 시각을 저장
numberRedisTemplate.opsForValue().set(FcfsUtil.keyFormatting(event.getId().toString()), event.getParticipantCount().intValue());
booleanRedisTemplate.opsForValue().set(FcfsUtil.endFlagFormatting(event.getId().toString()), false);
stringRedisTemplate.opsForValue().set(FcfsUtil.startTimeFormatting(event.getId().toString()), event.getStartTime().toString());
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

RedissonClient는 오직 Lock 관리의 이점(편리한 고수준 인터페이스와 자동 락 해제 기능 등)을 얻기 위해서만 활용하고, 기타 데이터 접근 부분은 모두 RedisTemplate을 통해 접근하도록 통일했습니다.

Comment on lines +27 to +45
String fcfsId = FcfsUtil.keyFormatting(eventSequence.toString());
if (isEventEnded(fcfsId)) {
return false;
}

// 이미 당첨된 사용자인지 확인
if(stringRedisTemplate.opsForZSet().rank(FcfsUtil.winnerFormatting(eventSequence.toString()), userId) != null){
throw new FcfsEventException(ErrorCode.ALREADY_WINNER);
}

// 잘못된 이벤트 참여 시간
String startTime = stringRedisTemplate.opsForValue().get(FcfsUtil.startTimeFormatting(eventSequence.toString()));
if(startTime == null) {
throw new FcfsEventException(ErrorCode.FCFS_EVENT_NOT_FOUND);
}

if (LocalDateTime.now().isBefore(LocalDateTime.parse(startTime))){
throw new FcfsEventException(ErrorCode.INVALID_EVENT_TIME);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

모든 구현체에서 이 부분이 중복되고 있어, 중복을 줄일 방안을 고민하고 있습니다. (아래 endEvent, isEventEnded도 마찬가지)
추상 클래스를 통해 문제를 해결하고자 시도했으나, 추상 클래스에 요구되는 의존성 주입과 실제 구현체에 요구되는 의존성 주입이 한 번에 이루어질 수 없다는 문제가 있어 대안을 찾고 있습니다.

Comment on lines +37 to +40
// 초기화
stringRedisTemplate.getConnectionFactory().getConnection().flushAll();
numberRedisTemplate.getConnectionFactory().getConnection().flushAll();
booleanRedisTemplate.getConnectionFactory().getConnection().flushAll();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

데이터 형식이 남아 계속 형식 관련 에러가 발생하고 있어 매 테스트마다 Redis 자체를 초기화해버리는 식으로 처리했습니다.

Copy link
Collaborator

@blaxsior blaxsior left a comment

Choose a reason for hiding this comment

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

고생하셨습니다. 초기에 같이 고민했던 구현 방안들이 하나씩 구현되는 것이 좋군요. 일관성을 지키면서도 성능이 좋은 방법을 찾는 것은 참 어려운 것 같네요!

Comment on lines +48 to +66
// 선착순 이벤트의 종료 여부 flag를 저장하기 위해 사용
@Bean
public RedisTemplate<String, Boolean> booleanRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Boolean> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}

// 선착순 이벤트의 참여 가능 인원수를 저장하기 위해 사용
@Bean
public RedisTemplate<String, Integer> numberRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Integer> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

이벤트 관련 정보도 redis에 담으면 사용자 요청마다 db에 접근할 필요가 없다는 점이 좋은 것 같습니다.

@win-luck win-luck merged commit c3bb5b1 into dev Aug 6, 2024
1 check passed
@win-luck win-luck changed the title [feat] 선착순 이벤트 구현방안 2: Redis SortedSet 기반 (#29) [feat] 선착순 이벤트 구현방안 2: Redis SortedSet + Lua 기반 (#29) Aug 6, 2024
@win-luck win-luck deleted the feature/29-feat-sortedset branch August 13, 2024 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat 기능 구현
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feat] 선착순 이벤트 구현방안 2: Redis SortedSet 기반 (#29)
2 participants