좋아요 비관적 락 적용 시 데드락 발생 관련 질문입니다..
spring boot: 3.3.2
mysql: 8.0.38
강사님과 동일한 버전 사용하고 있습니다.
@Test
void likePerformanceTest() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
likePerformanceTest(executorService, 1111L, "pessimistic-lock-1");
likePerformanceTest(executorService, 2222L, "pessimistic-lock-2");
// likePerformanceTest(executorService, 3333L, "optimistic-lock");
}
private void likePerformanceTest(ExecutorService executorService, Long articleId, String lockType) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3000);
System.out.println(lockType + " start");
long start = System.currentTimeMillis();
for (int i = 0; i < 3000; i++) {
long userId = i + 2;
executorService.submit(() -> {
like(articleId, userId, lockType);
latch.countDown();
});
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("lockType = " + lockType + ", time = " + (end - start) + "ms");
System.out.println(lockType + " end");
Long count = restClient.get()
.uri("/v1/article-likes/articles/{articleId}/count", articleId)
.retrieve()
.body(Long.class);
System.out.println("count = " + count);
}위 테스트를 실행 시키면
pessimistic-lock-1 start
lockType = pessimistic-lock-1, time = 5353ms
pessimistic-lock-1 end
count = 2991
pessimistic-lock-2 start
lockType = pessimistic-lock-2, time = 6667ms
pessimistic-lock-2 end
count = 2991count 가 3001이 안나오길래 서버 로그를 확인했는데,
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction이렇게 데드락이 발생됐다는 로그가 출력됐더라구요.. 강사님과 똑같은 코드로 실행한거 같은데, 왜 데드락이 발생한걸까요??
의심되는 부분은
@Transactional
public void likePessimisticLock1(Long articleId, Long userId) {
articleLikeRepository.save(
ArticleLike.create(snowflake.nextId(), articleId, userId)
);
int result = articleLikeCountRepository.increase(articleId);
if (result == 0) {
articleLikeCountRepository.save(
ArticleLikeCount.init(articleId, 1L)
);
}
}여러 스레드가 동시에 result == 0 조건에 진입해서 동일한 article_id 를 insert 하려고 하다가 충돌되는 과정에서 데드락이 발생된 것 같은데... 정확한 원인이 뭘까요? 제가 잘못 친게 있어서 그런걸까요..?
PessimisticLock1, PessimisticLock2 둘 다 데드 락이 발생한 원인이 궁금합니다!
게시물 생성 시점에 미리 0으로 count를 생성 해놓으면 괜찮을 것 같은데, 같은 코드로 실행했는대도 결과가 달라 질문남깁니다..
답변 2
1
chobo님, 안녕하세요!
일단 말씀주신 부분이 원인이 맞는 것 같습니다.
최초부터 동시 요청이 몰리면,
if (result == 0) 분기를 여러 동시 요청이 타게 되면서 데이터가 유실될 수 있습니다.
제가 제공해드린 코드를 보시면,
likePerformanceTest 테스트에서 멀티스레드로 for문 수행하기 전에 ,
like(articleId, 1L, lockType);
위 코드가 먼저 수행되는 부분을 확인하실 수 있을겁니다!
if (result == 0) 분기를 안타게 하기 위함입니다.
올려주신 코드에서는 위 코드가 누락되어 있네요.
그리고 강의에서는 해결책을 제시만 하고 넘어갔었는데요,
최초 요청 시에는 update 되는 레코드가 없으므로, 1로 초기화한다.
트래픽이 순식간에 몰릴 수 있는 상황에는 유실될 수 있으므로, 게시글 생성 시점에 미리 0으로 초기화 해둘 수도 있다.
이러한 전략에 대해 주석으로 남겨두고 있는 상황입니다!
위 전략을 직접 구성하진 않았지만, 실제로 글이 생성되자마자 좋아요 수가 엄청나게 몰리는 상황은 잘 없기도 해서 말로만 가볍게 언급하고 넘어간 부분이네요!
만약 그런 상황까지 가정해보고 싶다면 위 전략을 직접 구현해보실 수도 있습니다!
꼭 위 내용 아니더라도 해당 에러는 발생할 수 있을 것 같긴 한데요,
100개의 스레드풀로 동시 요청을 보내다보니, db에서는 동시에 100개의 요청에 대해 레코드 락을 걸고/해제해야 합니다.
처리가 빠르면 문제 없겠지만, 각 요청이 락을 점유하고 있는 시간이 길어진다면(로컬 장비 스펙에 따라 더 빠르고 느려질 수 있겠네요),
데드락이 발생할 수도 있을 것 같네요.
혹시 좋아요 데이터 개수와 좋아요 수가 일치한다면, 이 문제인 것 같네요.
그렇지 않다면 위에 언급된 result == 0 분기타는 문제고요!
또는 둘 다 발생한 상황일 수 있습니다.
저도 직접 세세하게 확인해본건 아니라 추측이긴 한데요, 이 부분들도 한번 검토해보시면 좋을 것 같습니다!
혹시 더 궁금한 점 있으시면 편히 문의 주세요!
0
안녕하세요, 인프런 AI 인턴입니다. chobo님.
데드락 문제는 데이터베이스에서 여러 트랜잭션이 서로가 필요한 리소스를 잠그고 기다리는 상황에서 발생할 수 있는 문제입니다. 특히, 비관적 락을 사용할 때 이러한 현상이 발생하기 쉬운데, 질문에서 설명해 주신 코드와 상황을 토대로 몇 가지 원인을 고려해볼 수 있을 것 같습니다.
데드락의 잠재적 원인
여러 스레드에서 동시에 동일한 데이터를 수정하려고 시도하는 경우입니다. 말씀하신 것처럼, 여러 스레드가
result == 0에 동시에 접근하여articleLikeCountRepository.increase메서드가 실패하고 이를 복구하려는 과정에서쿼리 충돌이 발생하여 데드락이 발생할 수 있습니다.락의 순차적 해제 문제: 두 개 이상의 트랜잭션이 잠금을 해제하는 순서가 서로 반대될 경우 데드락이 발생할 수 있습니다.
해결 방법
초기 상태 설정: 처음부터 article에 대한
likeCount를 0으로 설정하여increase가 항상 존재하는 레코드를 업데이트할 수 있게 합니다. 이는 데드락 발생을 줄이는 효과적인 방법 중 하나입니다.락 획득 순서 통일: 여러 락을 획득할 때에는 항상 동일한 순서로 락을 획득하도록 코드를 작성합니다. 이는 서로 다른 프로세스가 동일한 리소스를 기다리며 서로를 ‘교착’하는 상황을 피할 수 있게 합니다.
업데이트 시점 변경: 데이터를 변경하기 전에 모든 삽입이 완료되도록 하는 흐름제어를 통해 동시성 문제를 최대한 회피할 수 있습니다. 예를 들어, 트랜잭션의 범위를 줄이거나 커밋 타이밍을 조절하는 방법입니다.
비슷한 문제 해결과 관련된 자세한 예제나 유사한 사례에 대한 설명이 담긴 컨텐츠를 볼 수 있는 링크를 추천드립니다:
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드립니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
consumer에서 에러가 발생할 경우 데이터 유실 문의
0
42
2
게시글 테스트 데이터 삽입
0
47
2
정렬, 필터, 검색 등의 조건이 붙을 경우 최적화할 수 있는 방법이 무엇이 있을까요?
0
100
2
좋아요 기능 정합성 보장 방법
0
93
2
좋아요 동시성처리 최적의 선택?
1
121
2
프론트엔드 msa 환경 api 주소 통합? 과 서버끼리 통신 방식에대해
0
88
2
안녕하세요 무한스크롤 강의듣다가 질문이 있습니다.
0
57
1
조회수 조회 로직 질문
1
67
2
비로그인 유저도 어뷰징 방지 정책
1
65
2
CommentServiceTest의 테스트 로직 질문
0
53
2
무한 스크롤 방식에서 페이지 번호 방식 쿼리의 문제점 의문
1
72
2
path 쿼리 관련 질문드립니다!
0
59
2
antigravity 에디터를 쓰신다면 종료해주세요
1
122
0
프로젝트 구조
0
85
2
article_like_count api test
0
76
2
이벤트 페이로드 객체의 생성 방식이 팩토리 메서드 패턴이 아닌 빌더 패턴인 이유가 궁금합니다!.
0
103
2
[33. 좋아요 수 구현] 에서 테스트 하는 화면 동시성 문제
0
89
2
findByPath에서 articleId로도 검색을 해야 할 것 같아요.
0
88
3
jpa ddl-auto none을 하는 이유와 join 방법
0
95
2
팩터리 메소드와 response 객체 사용 이유가 궁금합니다!
0
90
2
커서 기반 페이지네이션 과 무한 스크롤링
0
101
2
게시글 생성 로직에서 오류 발생시 redis 게시글 수 되돌리기
0
96
2
멀티 모듈이 아닌 MSA 환경에서 common
0
150
2
2Depth 강의 도중 궁금한 점 있어요!!
0
73
2





