초기 서비스 운영과정에서 사용자가 얼마 없는 상황임에도 데드락 쿼리가 발생하였다. 데드락 쿼리가 발생한 코드 부분은 교착상태가 발생할 수 있는 조건이 없다고 생각했는데 해당 문제가 발생된게 의아하였다.
MYSQL 공식문서와 여러 블로그내용들을 참고하면서 결론은 양방향이 설정된 상태에서 외래키를 가지고 있는 테이블에 락이 전파되면서 데드락이 발생되었다.
해당글은 데드락발생 원인을 분석하고 개인적으로 정리한 글이다
게시판과 댓글처럼 다대일 양방향 관계가 맺어져 있었다
1@Entity2public class Reply {34 @ManyToOne(fetch = FetchType.LAZY)5 private Post post;67}89@Entity10public class Post {1112 @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)13 private Set<Reply> replies = new HashSet<>();1415 @Convert(converter = StepConverter.class)16 private Step step;1718}
사용자가 글을 올리면, 어떤 역할을 하는 담당자가 답변을 남길 수 있는 상황이다. 그리고 만약 담당자가 답변을 남기게 되면
사용자의 게시글 상태는 최초 게시글을 올린 상태인 게시글작성
에서 답변완료
상태로 변경이 된다.
그리고 물리적인 테이블 관계도는 답변테이블에 게시글 id값이 외래키로 설정되어 있었다.
1@Entity2public class Post {34 @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)5 private Set<Reply> replies = new HashSet<>();67 @Convert(converter = StepConverter.class)8 private Step step;910 public Reply addReply(Admin admin, String answer) {11 this.step = STEP.REPLY_COMPLETED;12 Reply reply = Reply.builder()13 .writer(admin.getId())14 .post(this)15 .answer(answer)16 .build();17 this.replies.add(reply);18 return reply; 1920 //.. 생략21}2223@Service24public class PostService {2526 private final postRepository postRepository;2728 @Transactional29 public void addReply(Admin admin, String answer, Long postId) {30 Post post = postRepository.findById(postId); 31 post.addReply(admin, answer);32 }33}
데드락 쿼리가 발생한 부분이 위와 같은 코드로 되어있었다. 위의 코드는 총 3번의 쿼리가 순차적으로 발생된다
INSERT, UPDATE 과정에서 쓰기락이 발생할 수 있지만, 트랜잭션이 겹치더라도 커밋이 완료되면 덮어씌우는 형태로 쿼리가 발생하여 문제가 없을텐데 어떤 부분에서 잘못되었는지를 알 수조차 없었다.
로컬에서 문제재현을 위하여 ExecutorService를 이용해 여러쓰레드를 만들어 테스트해보니 동일한 데드락 쿼리가 발생하였다.
해당 로그를 살펴보면 락을 획득할 수 없어 예외가 발생하였고, 해당 트랜잭션은 롤백되었다
라는 것을 알 수 있다.
로컬환경에서 디버깅모드로도 찾기가 어려워 이것저것 찾아보다가 mysql cli로 SHOW ENGINE INNODB STATUS
명령어를 수행하면
최근 발생된 데드락로그를 확인할 수 있었다.
1------------------------2LATEST FOREIGN KEY ERROR3------------------------42022-02-07 01:55:58 0x40e6538700 Error in foreign key constraint of table post/#sql-1_40:5 foreign key (post_id) references post:6Syntax error close to:78------------------------9LATEST DETECTED DEADLOCK10------------------------112022-02-07 01:57:05 0x4163d9a70012*** (2) TRANSACTION:13TRANSACTION 1, ACTIVE 0 sec starting index read14mysql tables in use 1, locked 1155 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 116MySQL thread id 67, OS thread handle 280848082688, query id 2621 172.17.0.1 root updating17update post set step=2 WHERE id = 118*** (2) HOLDS THE LOCK(S):19RECORD LOCKS space id 4338 page no 3 n bits 72 index PRIMARY of table `example`.`post` trx id 1 lock mode S locks rec but not gap20Record lock, heap no 2 PHYSICAL RECORD: n_fields 15; compact format; info bits 02122*** (2) WAITING FOR THIS LOCK TO BE GRANTED:23RECORD LOCKS space id 4338 page no 3 n bits 72 index PRIMARY of table `example`.`post` trx id 1 lock_mode X locks rec but not gap waiting24Record lock, heap no 2 PHYSICAL RECORD: n_fields 15; compact format; info bits 02526// 생략...27*** WE ROLL BACK TRANSACTION (2)28------------29TRANSACTIONS30------------
LATEST FOREIGN KEY ERROR
: 외래키 데이터 정합성 체크 중 발생된 에러인것을 확인 WAITING FOR THIS LOCK TO BE GRANTED
: Lock을 선점하기 위해 대기중인 데이터 정보HOLDS THE LOCK
: 현재 확보 중인 Lock에 대한 정보 위에 로그에서 데드락이 발생된 원인은 S락을 획득한 상태에서 게시판 상태 업데이트를 위해 X락을 획득을 시도한다. X락을 선점하기 위해서는 상대방이 가지고 있는 S락이 해제되어야 X락을 획득할 수 있으나, 계속 상대방 트랜잭션이 S락을 선점하고 있어 데드락이 발생된걸로 확인된다.
여기서 이해가 안됬던 부분은 총 3가지이다. 데드락 개념, S락에 대한 이해, S락 발생 원인
SELECT ... LOCK IN SHARE MODE
구문을 사용할 때 위의 분석된 내용을 정리해보면 답변 레코드를 INSERT 후 게시글 상태를 UPDATE 과정에서 답변 테이블에 있는 외래키 제약조건으로 부모테이블인 게시글 레코드에 공유락이 설정되고, 트랜잭션이 겹친 상황에서 여러 트랜잭션은 공유락을 같이 선점할 수 있지만, 게시글 레코드를 업데이트하기 위해 X락 선점을 하려면 상대방이 가지고 있는 공유락이 해제되어야 X락을 획득할 수 있으나, 서로가 S락 해제를 대기하게 되어 데드락이 발생된걸로 확인되었다.
외래키를 삭제한다