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

[LIME-71 ] 투표 참여 API 데드락 & 동시성 이슈 해결 #75

Merged
merged 4 commits into from
Apr 1, 2024

Conversation

Yiseull
Copy link
Member

@Yiseull Yiseull commented Mar 21, 2024

📌 PR 종류

어떤 종류의 PR인지 아래 항목 중에 체크 해주세요.

  • 🐛 버그 수정
  • ✨ 기능 추가
  •  테스트 추가
  • 🎨 코드 스타일 변경 (formatting, local variables)
  • 🔨 리팩토링 (기능 변경 X)
  • 💚 빌드 관련 수정
  • 📝 문서 내용 수정
  • 그 외, 어떤 종류인지 기입 바람:

📌 어떤 기능이 추가 되었나요?

Issue Number

LIME-71

❎ 문제 상황

  • 투표 참여 API를 여러 명이 요청할 경우, 데드락과 동시성 문제가 동시에 발생합니다.

1️⃣ 데드락 해결

데드락이 발생한 이유

  • 투표 참여 로직에는 voter insert, vote update가 발생합니다. voter와 vote는 자식-부모 관계로 연관관계를 맺고 있습니다.
  • MySQL InnoDB는 외래키 제약조건을 지키기 위해 외래키 컬럼에 대해 락이 전파됩니다.
  • voter가 insert 될 때 vote 테이블에서 (insert에 있는) voteId를 가진 레코드에 대해 공유락이 걸리고(락 전파), 공유락이 걸린 상태로 update가 이루어져 데드락이 발생합니다.

해결 방법

  1. voter insert를 flush한 후, vote update 실행
  2. voter와 vote 사이에 연관관계 제거 (외래키 제거)

결론

  • 2번 데드락의 근본적인 원인인 외래키를 제거하는 방법으로 해결하였습니다.
  • 1번을 선택하지 않은 이유는 아래와 같습니다.
    • flush 사용에 익숙하지 않은 상태에서 사용한다면 의도치 않은 문제가 발생할 수도 있습니다.
    • 코드를 처음 보는 사람이 flush를 본다면 왜 flush가 사용되었는지 한 눈에 알아보기 어려울 것입니다.

2️⃣ 동시성 이슈 해결

동시성이 발생한 이유

  • 투표가 마감되기 전에(투표 인원 수가 다 차기 전에) 여러 트랜잭션이 실행되기 때문에 동시성이 발생하였습니다.

해결 방법

insert는 기존의 비교할 레코드가 없어 낙관적 락, 비관적 락 사용이 불가능하다고 판단하였습니다. 또한 syncronized는 다른 voteId에 대해서도 락이 걸리기 때문에 해결 방법에서 제외하였습니다.

  1. MySQL - 네임드락
  2. Lettuce - setnx 명령어를 활용한 스핀락
  3. Redisson - pub-sub 기반의 락

결론

  • 2번 Lettuce의 스핀락을 구현하여 동시성 이슈를 해결하였습니다.
  • 1번을 선택하지 않은 이유는 네임드락을 통해 동시성을 해결하려면 트랜잭션 2개가 필요합니다. 지금 설정되어있는 커넥션 풀 설정로 네임드락을 사용하면 커넥션이 부족한 이슈가 발생하여 선택하지 않기로 했습니다. 커넥션 풀 사이즈를 늘려도 되지만 어느 정도로 늘려야 할지 적절한 사이즈를 선택하기 어렵고, 1000개까지 늘려서 1000명 동시성 테스트를 해도 테스트가 실패하였습니다.
  • 3번을 선택하지 않은 이유는 Redisson을 사용하기 위해 라이브러리를 추가해줘야하고, 현재 멀티 모듈 구조에서 Redisson을 사용하기 복잡해서 사용하지 않았습니다. Redisson 락을 통해 구현하려면 RedissonClient를 사용해야 합니다. RedissonClient는 인프라 모듈에서만 사용할 수 있고, 투표 참여 로직을 사용하기 위해서는 구현 계층의 VoteManager를 사용해야 합니다. 따라서 Redisson 락을 이용하려면 중간 매개체 클래스들이 여러 개 필요하기 때문에 더 복잡해질 수 있습니다.

📌 기존에 있던 기능에 영향을 주나요?

  • 아니요

@Yiseull Yiseull added bug Something isn't working test This will not be worked on labels Mar 21, 2024
@Yiseull Yiseull self-assigned this Mar 21, 2024
@Curry4182
Copy link
Contributor

며칠 동안 글 작성한 내용 찾아보면서 공부했는데 유익했습니다!

Copy link
Member

@HandmadeCloud HandmadeCloud 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 +24 to +25
while (Boolean.FALSE.equals(voteRedisManager.lock(String.valueOf(vote.getId())))) {
Thread.sleep(10);
Copy link
Member

Choose a reason for hiding this comment

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

10ms마다 검사하는 방법이 가장 효율적이었던건지 궁금해서 질문드립니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

제가 생각할 때는 10이 가장 적절한 것 같습니다!

락을 얻기 위해 계속 시도하면 레디스에 부하를 주기 때문에 Thread.sleep()으로 약간의 시간차를 두고 락 획득을 시도하도록 하였습니다. 다만 이 시간을 어떻게 설정해야 될까 고민을 많이 했고, 100, 50, 10 이렇게 3가지를 고려했습니다. 아무래도 처리 로직이 간단하기 때문에 처리 속도도 빠릅니다. 따라서 tps량이 가장 많은 10으로 선택을 했고, 레디스가 그 정도 부하는 충분히 버틸 수 있다고 생각했습니다! 10으로도 문제 없이 처리할 수 있는데 100과 50으로 시간을 설정하는 것은 오히려 응답 시간을 지연시킬 수 있다고 생각했습니다.

@Yiseull Yiseull merged commit 846f749 into main Apr 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working test This will not be worked on
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants