게시물의 좋아요 기능을 JPA의 더티 체킹 기능(=변경 감지)으로 작동시키고 있었다. 그러다 문득 든 생각이 만약 동시에 여러 요청이 들어왔을때 좋아요 개수의 정합성이 맞을까? 라는 생각을 하고 테스트를 진행했다.
@Test
@DisplayName("게시물 좋아요 동시성 테스트")
public void like_post_concurrent_threadCount() throws Exception {
int threadCount = 10000;
//given
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
User user = mockUser();
User githubUser = githubUser();
AuthUser authUser = AuthUser.of(githubUser);
Post post = Post.of("테스트 제목","테스트 본문",user);
given(postRepository.findByIdAndStatus(any(),any()))
.willReturn(Optional.of(post));
//when
for(int i=0; i<threadCount; i++){
executorService.submit(()->{
try{
postService.likePost(authUser,post.getId());
}
finally {
latch.countDown();
}
});
}
latch.await();
//then
assertThat(post.getLikeCnt()).isEqualTo(threadCount);
}
위와 같이 10000번의 요청이 동시에 들어오게되면?
만약 100번의 요청이 동시에 들어오게 되면?
좋아요 개수가 맞지 않는 결과를 볼 수 있었다.
서버가 여러개일 때 그리고 동시에 API 요청이 들어올 때
더티체킹은
그러면 멀티쓰레딩 환경인 스프링에서 트랜잭션이 커밋되기전에 만약 조회가 된다면? 데이터 정합성이 안맞을 수 있다.
낙관적 락
비관적 락
쿼리 직접 실행
Redisson
쿼리 직접 실행과 Redisson을 이용해 본다.
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value="update Post p set p.likeCnt = p.likeCnt + 1 where p.id = :postId")
void increaseLikeCount(@Param("postId") Long postId);
100번의 요청 결과
10000번의 요청 결과
Redisson 라이브러리 추가
implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.19.0'
파사드 패턴을 이용한 클래스 추가
@Component
@RequiredArgsConstructor
@Slf4j
public class RedissonLockPostFacade {
private final RedissonClient redissonClient;
private final PostService postService;
public void likePost(AuthUser authUser, Long postId){
RLock lock = redissonClient.getLock(postId.toString());
try {
// 획득시도 시간, 락 점유 시간
boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS);
if (!available) {
log.info("lock 획득 실패");
return;
}
postService.likePost(authUser,postId);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
• Lock을 해제하기 전에 COMMIT을 해줘야 동시성 문제가 발생하지 않는다.
10000번 요청한 결과
@Test
@DisplayName("게시물 좋아요 / Redisson")
public void like_post() throws Exception {
int threadCount = 10000;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
//given
User user = mockUser();
userRepository.save(user);
User githubUser = githubUser();
userRepository.save(githubUser);
AuthUser authUser = AuthUser.of(githubUser);
Post post = createPost("테스트 제목","테스트 내용",user);
postRepository.save(post);
//when
for(int i=0; i<threadCount; i++){
executorService.submit(()->{
try{
redissonLockPostFacade.likePost(authUser,1L);
}
finally {
latch.countDown();
}
});
}
latch.await();
//then
Post result = postRepository.findById(1L).get();
assertThat(result.getLikeCnt()).isEqualTo(threadCount);
}
redis 자원을 이용해 Redisson을 이용할만큼 좋아요 개수가 서비스에 유의미한 영향을 주는 요소가 아니라 판단되어 쿼리 직접 실행을 채택한다.
참고