해결된 질문
작성
·
209
1
학습 관련 질문을 최대한 상세히 남겨주세요!
고민 과정도 같이 나열해주셔도 좋습니다.
먼저 유사한 질문이 있었는지 검색해보세요.
인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요.
안녕하세요! 강의 재밌게 잘 듣고 있는 취준생입니다!
테스트 실행 도중에 데드락이 발생하여 이에 관해 여쭤보고자 합니다.
구체적으로인 상황은
article_view_count
테이블에 존재하지 않는 레코드를 삽입
AND
어플리케이션 실행 후, 첫 테스트 코드 실행
을 만족하는 상황에서 DeadLock 에러가 발생합니다. 아마 Lock 획득실패로 인한 에러가 표출되는것으로 보이며
에러 로그는 하단에 첨부했습니다.
제가 생각한 원인은 다음과 같습니다.
UPDATE문임에 따라 해당 레코드에 X-Lock이 걸리며,
100개의 요청마다 UPDATE쿼리가 날라가지만, 첫 INSERT 요청의 트랜잭션이 완료되지 않아 이후의 트랜잭션이 잠금 대기
롤백
첫 INSERT 요청이 오래 걸리는 이유를 알고 싶습니다.
양질의 강의 제공해주셔서 감사합니다!
2025-05-01T19:46:27.647+09:00 WARN 27109 --- [kuke-board-view-service] [io-9003-exec-63] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1213, SQLState: 40001
2025-05-01T19:46:27.647+09:00 ERROR 27109 --- [kuke-board-view-service] [io-9003-exec-63] o.h.engine.jdbc.spi.SqlExceptionHelper : Deadlock found when trying to get lock; try restarting transaction
Hibernate: update article_view_count set view_count = ? where article_id = ? and view_count < ?
2025-05-01T19:46:27.653+09:00 ERROR 27109 --- [kuke-board-view-service] [io-9003-exec-63] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [insert into article_view_count (view_count,article_id) values (?,?)]; SQL [insert into article_view_count (view_count,article_id) values (?,?)]] with root cause
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
답변 2
0
희종님, 안녕하세요!
강의 재밌게 잘 들어주셔서 감사합니다!
아직 삽입되지 않은 레코드에 대해서 레코드 락이 걸리진 않을 것 같습니다!
다른 곳에 원인이 있을 것 같은데요, 강의와 코드가 동일한 상황이 맞을까요?
insert문 로그도 두 번 남아있는데, 어떤 시나리오에서 첫 INSERT 요청이 병목되고 있던 것일지 이해가 어렵네요.
작성하신 코드(서비스/리포지토리 등 테스트 포함)도 같이 올려주시면 파악에 도움 될 것 같습니다!
희종님, 공유 감사합니다!
ArticleViewCountBackUpProcessor도 부탁드립니다!!
특이하게도 ViewApplication을 실행하고 첫 실행에서만 해당 에러가 발생했습니다.
특정 파라미터에 대해 테스트코드 첫 실행이 아니라,
애플리케이션 첫 실행에만 발생하는 문제라는 말씀이실까요?
@Slf4j
@Component
@RequiredArgsConstructor
public class ArticleViewCountBackUpProcessor {
private final ArticleViewCountBackUpRepository articleViewCountBackUpRepository;
@Transactional
public void backUp(Long articleId, Long viewCount) {
int result = articleViewCountBackUpRepository.updateViewCount(articleId, viewCount);
if (result == 0) {
articleViewCountBackUpRepository.findById(articleId)
.ifPresentOrElse(
ignored -> {},
() -> articleViewCountBackUpRepository.save(ArticleViewCount.init(articleId, viewCount))
);
}
}
}
늦은 시간에 감사합니다!
음 제가 보기에도 코드는 동일해서 육안으로는 파악이 힘들군요..!
계속 번거롭게 해드려서 죄송하지만,
제 프로필 누르면 나오는 메일로 프로젝트 전체 코드 압축해서 보내주실 수 있을까요!?
직접 실행해서 확인해보려고 합니다!
특정 파라미터에 대해 테스트코드 첫 실행이 아니라,
애플리케이션 첫 실행에만 발생하는 문제라는 말씀이실까요?
두 상황 모두 만족할때 이와 같은 에러가 발생했습니다!
예를 들어,
어플리케이션 첫 실행이더라도, 데이터베이스에 해당 레코드가 이미 있는 경우(이미 존재하는 article_id
)엔 데드락 에러가 발생하지 않았습니다.
엇 메일이 아직 안온 것 같습니다. 다시 확인해보시겠어요? kukekyakya@gmail.com
사이즈 문제였는지, 보안 문제였는지 잘 기억은 안나는데, 메일 전송이 잘 안되다고 하셔서 구글드라이브 등으로 따로 업로드해서 전달주시기도 하더라고요.
희종님, 코드 확인 했습니다. 감사합니다!
사실 제 로컬 환경에서 재현은 안되는데, 원인 파악을 위해 아래와 같이 ArticleViewCountBackUpProcessor.backUp에 sleep 메소드를 걸어보았습니다.
@Transactional
public void backUp(Long articleId, Long viewCount) {
int result = articleViewCountBackUpRepository.updateViewCount(articleId, viewCount);
if (result == 0) {
articleViewCountBackUpRepository.findById(articleId)
.ifPresentOrElse(
ignored -> {},
() -> articleViewCountBackUpRepository.save(ArticleViewCount.init(articleId, viewCount))
);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
위처럼 sleep을 걸어보니 저도 다음과 같은 에러가 발생하더라고요.
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
원래는 ArticleViewCountBackUpProcessor.backUp이 멀티스레드에서 동시에 처리되고 있는 상황까지 가정하진 않았는데,
카운트 증가 요청이 빠르게 들어오면서 backUp 메소드가 동시에 처리되고 있는 상황이군요..!
제 로컬 환경에서는 재현되지 않는 것으로 보아, 희종님 로컬 환경에서 mysql 처리되는 속도가 비교적 느려서 발생하는 것 같습니다!
문제 상황을 정리해보면,
첫번째 100개가 채워졌을때, 트랜잭션이 열려서 insert 문을 수행하는데 아직 커밋이 안된 상황이라고 가정해보겠습니다.
이후 두번째 100개가 채워졌을때에도 트랜잭션이 열리는데, 첫번째 트랜잭션이 아직 커밋되지 않아서 update 쿼리는 0을 반환하고 insert 문을 중복으로 수행하게 됩니다.
하지만 첫번째 트랜잭션이 아직 커밋되지 않아서 락을 물고 있기 때문에, 두번째 트랜잭션은 데드락이 발생합니다.(락에 대해서는 아래에서 다시 설명합니다.)
희종님의 로컬 테스트 환경에서 mysql이 처리되는 속도에 비해 동시 요청이 너무 빠르게 들어와서 그런 것 같은데요!
Executors.newFixedThreadPool(10)
ViewApiTest에서 병렬성을 줄이기 위해 멀티스레드 수를 100개에서 10~30개 정도로 줄여보시겠어요?
현재 100개의 스레드풀로 처리되는 테스트 환경은, 게시글이 작성되자마자 밀리세컨드 단위로 순식간에 200개의 조회수 증가 요청이 들어올 수 있다는 것인데, 어지간히 큰 서비스가 아닌 이상 사실 이처럼 처리될 일은 드물긴 합니다.(백업 한번 실패하더라도, 이후 백업 시점에 어차피 다시 정상 처리될 것이고요)
그래서 지금은 원활한 테스트를 위해서 임의로 줄여보는 것이고, 실무에서는 여러 개의 트랜잭션이 동시에 처리될 일이 없도록 백업에 대해서는 비동기로 순차 처리할 수도 있고, 게시글 생성 시점에 article_view_count 레코드를 0으로 미리 초기화해둘 수도 있습니다. (물론, 이러한 방법들은 오버엔지니어링일 수 있어서 학습 예시일 뿐이라는 점 감안해주시면 좋을 것 같고, 비동기로 처리한다는 개념에 대해서는 인기글 챕터 학습하다보면 이해되실 것 같습니다!)
아무튼 그래서 왜 데드락이 발생했냐면, gap lock이 걸린 것 같습니다.
이미 존재하는 단일 레코드에 락을 잡는 record lock과 달리, gap lock은 인덱스의 어떠한 범위에 대해 락을 잡는 것을 의미합니다.
첫번째 트랜잭션은 특정 pk에 대해 insert를 하기 위해서 gap lock을 걸게 됩니다.
다른 동시 트랜잭션에서 새로운 레코드가 동일한 위치에 삽입되는 것을 막기 위함입니다.
이후에 두번째 트랜잭션에서 레코드 삽입을 시도하고자 하지만, 첫번째 트랜잭션에서 gap lock을 잡고 있기 때문에 데드락이 발생하게 된 것입니다.
조금 더 자세한 정보를 분석해보고 싶으시면,
show engine innodb status;
mysql 데이터베이스에 접속해서 위와 같은 구문을 입력한 뒤에 LATEST DETECTED DEADLOCK 영역을 보시면, 최근에 감지된 데드락을 확인해보실 수 있습니다.
제 로컬에서 확인해본 것인데,
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 11 page no 4 n bits 208 index PRIMARY of table `article_view`.`article_view_count` trx id 62692 lock_mode X locks gap before rec
lock_mode X locks gap before rec을 보시면 gap lock이 잡혔다는 것을 확인해볼 수 있었습니다.
수강생 분들 각각의 로컬 환경이 다른 것인데, 모든 테스트 환경을 잡아내진 못했던 것 같네요.
다음 백업 시점에 도달하는 순간에도 첫번째 mysql 트랜잭션이 아직 처리 안되고 있는 상황은 저도 예상을 못했네요..! (레디스의 처리 속도를 생각하면 충분히 가능한 상황이긴 한데, 제 로컬에서 발생이 안되어서 인지를 잘 못했네요 ㅠ)
원인 파악이 되었다면, 현 시점에 당장 해결책까지 적용할만한 부분은 아닌 것 같고(문제가 치명적이지도 않고, 해결 방법 자체가 어렵진 않으므로), 그냥 그런가보다 인지하고 넘어가셔도 괜찮을 것 같습니다!
강의 수강에 불편을 드려 죄송합니다..!
정성스런 답변 감사합니다 !
이후에 두번째 트랜잭션에서 레코드 삽입을 시도하고자 하지만, 첫번째 트랜잭션에서 gap lock을 잡고 있기 때문에 데드락이 발생하게 된 것입니다.
이 부분이 잘 이해가 되지 않습니다.
데드락이 발생하려면 사이클이 발생해야 하는데, 갭락 경합만으론 데드락이 발생하지 않는게 아닌가요? ㅠㅠ
순환부분이 어디인지 알려주실 수 있을까요?
질척거려서 죄송합니다 ㅠ
+) 알려주신 명령어를 통해 출력된 결과는 아래와 같았습니다.
2025-05-01 14:37:10 281473177861888
*** (1) TRANSACTION:
TRANSACTION 109962, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 1210, OS thread handle 281472901041920, query id 16154679 192.168.65.1 root update
insert into article_view_count (view_count,article_id) values (400,602)
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 7 page no 4 n bits 88 index PRIMARY of table `article_view`.`article_view_count` trx id 109962 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 4 n bits 88 index PRIMARY of table `article_view`.`article_view_count` trx id 109962 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 109960, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 1208, OS thread handle 281472401207040, query id 16154681 192.168.65.1 root update
insert into article_view_count (view_count,article_id) values (200,602)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 7 page no 4 n bits 88 index PRIMARY of table `article_view`.`article_view_count` trx id 109960 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 4 n bits 88 index PRIMARY of table `article_view`.`article_view_count` trx id 109960 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
제가 이 내용을 봤을 땐, 각 트랜잭션이 갭락을 가지고 있는것으로 보이는것만 인지할 수 있었습니다. ㅠㅠ
https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html
확인했습니다. 🙂 시간내주셔서 감사합니다 ㅎㅎ
로그를 정확히 분석해보면,
트랜잭션 1은 동일 범위에 대해 갭락
트랜잭션 2는 동일 범위에 대해 갭락
트랜잭션 1은 특정 pk에 대해 삽입 시도 - 이때는 트랜잭션 2의 갭락에 의해 insert intention lock 대기
트랜잭션 2는 특정 pk에 대해 삽입 시도- 이때는 트랜잭션 1의 갭락에 의해 insert intention lock 대기
위와 같은 상황 때문에 데드락이 발생했다는 것을 알 수 있습니다.
insert intention lock은 삽입 전에 특정 레코드에 삽입하겠다고 의도를 나타내는 락을 의미합니다. (gap lock의 한 종류)
https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html#innodb-insert-intention-locks
두 트랜잭션은 동시에 gap lock을 점유하고, 각 트랜잭션은 삽입 전에 insert intention lock을 한번 더 잡게 되는데, 타 트랜잭션에서 점유된 gap lock에 의해 락을 획득하지 못하고 대기 상태에 빠지게 됩니다.
그래서 데드락이 발생하게 됩니다.
예시로 타임라인을 만들어보겠습니다.
*** (1) TRANSACTION:
1번 트랜잭션이 시작됩니다.
*** (1) HOLDS THE LOCK(S):
1번 트랜잭션이 gap lock을 잡습니다. 어떠한 gap에 대해 락을 획득합니다.
*** (2) TRANSACTION:
2번 트랜잭션도 시작됩니다.
*** (2) HOLDS THE LOCK(S):
2번 트랜잭션도 gap lock을 점유합니다.
여기까지 보면, 1번과 2번 트랜잭션 모두 gap lock을 동시에 점유한 것입니다.
왜 gap lock을 동시에 점유하는게 가능하냐면, insert intention lock이라는 개념을 만들어뒀기 때문인 것 같네요.
이러한 개념은 여러 트랜잭션이 동일한 gap에 대해 삽입을 해야할 경우, gap 내의 동일한 위치가 아니라면 서로 기다릴 필요 없이 처리가 가능하도록 하기 위해 만들었다고 하네요. 그래서 허용되는 동작 같습니다.
(여기는 강의 주제나 애플리케이션 개발자의 영역을 벗어나는 너무 심화 내용 같은데, 제가 데이터베이스 개발자나 DBA는 아니다보니 로그나 문서 기반으로 추측한 동작입니다.)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 4 n bits 88 index PRIMARY of table `article_view`.`article_view_count` trx id 109962 lock_mode X insert intention waiting
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 4 n bits 88 index PRIMARY of table `article_view`.`article_view_count` trx id 109960 lock_mode X insert intention waiting
1번과 2번은 insert intention lock을 잡아야하는데, 서로 다른 트랜잭션에 의해 잡혀있는 gap lock 때문에 insert intention lock을 점유하지 못하고 대기하게 됩니다.
그래서 데드락을 해소하기 위해서는, 둘중 하나의 트랜잭션은 롤백이 필요합니다.
공유주신 문서에도 예시가 잘 나와 있군요!
https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html
확인되어서 다행입니다!
정확히는 최초 트랜잭션에서 x lock이 잡혀있을때, 타 트랜잭션에서는 중복키를 감지하고 s lock으로 점유하게 되어서 발생 가능한 것이군요.
요 동작은 저도 처음 알았네요~
사실 저도 100% 다 이해한건 아니지만.. DB 세부적인 동작이나 개발 의도까지 딥하게 다 알 수도 없고 알 필요도 없다보니 여기까지만.. ㅎㅎ 재밌게 잘 봤습니다!
이 늦은 시간까지 답변 해주시고 계셨군요 ㅠㅠ
정성에 감동받았어요!!
너무 머리가 아팠네요 ㅋㅋㅋㅋㅋ
쓸데없는 질문에도 같이 고민해주셔서 너무 감사합니다!!!
남은 강의도 재밌게 완주하겠습니다. 🙂 ㅎㅎㅎㅎ
0
안녕하세요, 인프런 AI 인턴입니다. 윤희종님.
데드락 문제는 트랜잭션이 서로 다른 트랜잭션에 의해 잠긴 리소스를 기다리는 상태에서 발생할 수 있으며, 특히 여러 스레드가 동시에 동일 테이블의 레코드를 수정하려고 할 때 더욱 빈번하게 발생합니다.
첫 번째 INSERT 잠금 문제: 처음 article_view_count
테이블에 데이터가 없을 때, 새로운 엔트리를 삽입하려는 동시에 다수의 트랜잭션이 해당 테이블에 대한 잠금을 시도할 수 있습니다. 이로 인해 첫 번째 삽입 트랜잭션이 완료되지 않고, 이후에 발생하는 UPDATE
요청들이 대기하면서 데드락이 발생할 수 있습니다.
해결 방법:
view_count
를 0으로 설정)를 사전에 삽입하여, 첫 번째 INSERT
요청이 데드락을 유발하지 않도록 합니다.READ COMMITTED
수준으로 설정하여, 다른 트랜잭션이 아직 커밋하지 않은 변경 사항에 대한 읽기를 방지할 수 있습니다.비슷한 Deadlock 사례와 해결 방법에 대한 유용한 정보를 제공할 수 있는 링크를 추천드립니다:
- Deadlock 방지 기법 및 트랜잭션 관리
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 문제 해결에 유사한 링크를 제공하고 있습니다. 현재 베타 기능으로 답변이 완벽하지 않을 수 있는 점 양해 부탁드리며, 추가적인 궁금한 점이 있으시면 자유롭게 질문해 주세요. 곧 지식공유자께서 추가적인 답변을 제공해 드릴 것입니다.
안녕하세요 쿠케님!
강의랑 제 코드를 다시 검토해본 결과, 일치한다고 판단되나 혹시 몰라 남기겠습니다...!
ViewApplication
인스턴스를 실행하고ViewApiTest
에서 새로운articleId
로 요청 시, 데드락이 발생했습니다.부족한 설명이라 죄송할 따름이네요 ㅠ
+) 특이하게도 ViewApplication을 실행하고 첫 실행에서만 해당 에러가 발생했습니다.