db

데드락 이슈(외래키 s-lock)

kivv00ng 2023. 3. 20. 20:25

ERD

hyper-link(통합플랫폼)의 테이블 구조는 member와 content의 다대다 관계를 막고자 중간에 member_content테이블을 두었다. 그래서 좋아요 클릭시 member_content의 컬럼이 insert되고, content테이블의 like_count컬럼 값이 증가한다.

 

이를 바탕으로 좋아요 API는 memberContent 저장 후, content테이블의 like_count를 증가하는 로직으로 구현하였다. 구현 후에, 동시성 테스트를 진행하는데, 데드락이 발생했다.

찾아보니 외래키가 존재하는 자식 테이블의 insert, update, delete시 부모테이블의 존재를 확인하기 위해 해당 컬럼에 s-lock을 건다고 한다. 따라서 아래와 같은 상황이 발생한 것으로 예상된다.

각 스레드가 서로 x-lock 걸기위해 반대편의 s-lock이 끝나기를 기다리는 것이다.

따라서 위의 로직을 'insert-update' 순서에서 'update-insert' 순서로 변경하여 x-lock을 먼저 걸어주도록 했다.

 

수정 전 코드

@Transactional
public LikeClickResponse clickLike(Long memberId, Long contentId,
  LikeClickRequest likeClickRequest) {
    if (likeClickRequest.addLike()) {
      return createLike(memberId, contentId);
    }
return deleteLike(memberId, contentId);
}

private LikeClickResponse createLike(Long memberId, Long contentId) {
    existLike(memberId, contentId);
    Content foundContent = contentService.findById(contentId);
    memberContentRepository.save(new MemberContent(memberId, foundContent, LIKE));
    int likeCount = contentService.addLike(contentId); //변경 코드
    return new LikeClickResponse(likeCount);
}

private LikeClickResponse deleteLike(Long memberId, Long contentId) {
    ...
}

 

수정후 코드

@Transactional
public LikeClickResponse clickLike(Long memberId, Long contentId,
  LikeClickRequest likeClickRequest) {
    if (likeClickRequest.addLike()) {
      return createLike(memberId, contentId);
    }
return deleteLike(memberId, contentId);
}

private LikeClickResponse createLike(Long memberId, Long contentId) {
    existLike(memberId, contentId);
    int likeCount = contentService.addLike(contentId); //변경 코드
    Content foundContent = contentService.findById(contentId);
    memberContentRepository.save(new MemberContent(memberId, foundContent, LIKE));
    return new LikeClickResponse(likeCount);
}

private LikeClickResponse deleteLike(Long memberId, Long contentId) {
	...
}

테스트 코드

  @BeforeAll
  void setup() {
    ...
  }

  @AfterAll
  void delete() {
	...
  }
  
  
  @Test
  @DisplayName("여러명이 동시에 좋아요를 눌러도 의도한 값을 얻을 수 있다.")
  void multiThreadLikeClickTest() throws InterruptedException {
    int threadCount = 10;
    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
    CountDownLatch latch = new CountDownLatch(threadCount);

    LikeClickRequest likeClickRequest = new LikeClickRequest(true);

    for (int i = 0; i < threadCount; i++) {
      final int index = i;
      executorService.execute(() -> {
        likeService.clickLike(members.get(index).getId(), content.getId(), likeClickRequest);
        latch.countDown();
      });
    }
    latch.await();

    Content foundContent = contentRepository.findById(content.getId())
        .orElseThrow(ContentNotFoundException::new);
    assertThat(foundContent.getLikeCount()).isEqualTo(threadCount);
  }