엔티티 구조
Debate, DebateMember 엔티티가 있고, DebateMember에서 Debate에 대한 외래키를 갖는다.
@Entity
@Getter
@Table(name = "debate_member")
@NoArgsConstructor
public class DebateMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "deb_mem_id")
private Long debMemId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deb_id")
private Debate debate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "u_id")
private User user;
}
데드락 경험기
데드락이 발생하는 것을 확인하기 위해 작성한 코드이다.
실행 의미는 딱히 없고 Service에서 @Transactional 안에서 update, insert가 발생하는 것만 유의깊게 보면 된다.
@Test
public void testConcurrent() throws InterruptedException{
int numThreads= 10;
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
Long[] userList = {4L, 5L};
for(int i =0;i<2;i++){
int finalI = i;
String url = "http://localhost:8080/debate/test/"+userList[finalI];
executorService.submit(()->{
ResponseEntity result = restTemplate.getForEntity(url, null);
System.out.println(result.getStatusCode());
});
}
executorService.shutdown();
}
public class DebateController {
private final DebateService debateService;
@GetMapping("/debate/test/{u-id}")
public void test (@PathVariable("u-id") Long userId) {
try{
debateService.testMethod(userId);
}catch (Exception e){
e.printStackTrace();
}
}
}
public class DebateService {
private final UserRepository userRepository;
private final DebateRepository debateRepository;
@Transactional
public void testMethod(Long userId){
Long debateId = 104L;
Debate debate = debateRepository.findDebatesTest(debateId);
debate.setDebateDescription("수정했음."); // update
User user = userRepository.getReferenceById(userId);
Debate newDebate = debateRepository.getReferenceById(debateId);
DebateMember debateMember = new DebateMember(newDebate, user);
debateMemberRepository.save(debateMember); // insert
}
}
@Repository
public interface DebateRepository extends JpaRepository<Debate, Long> {
@Query("select deb from Debate deb left join fetch deb.debateMembers where deb.debateId= :DebateId")
Debate findDebatesTest(@Param("DebateId") Long DebateId);
}
테스트를 실행해보면 데드락이 발생한 것을 확인할 수 있다.
그 해답은 MySQL 공식 문서에서 찾을 수 있다.
MySQL은 foreign key가 존재하는 테이블에서 foreign key를 포함한 레코드를 삽입, 수정, 삭제 하는 경우 제약 조건을 확인하기 위해 s-lock을 설정한다.
즉, DebateMember에 insert할 때 Debate, User에 대한 fk가 걸려있기 때문에 이 제약조건을 확인하기 위해 s-lock이 걸린 것이다.
record를 수정할 때 항상 x-lock을 건다.
즉, Debate를 update할 때 x-lock이 걸린 것이다.
show engine innodb status;
를 통해 mysql의 최근 데드락 로그를 확인할 수 있다.
------------------------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 40660, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 23832, OS thread handle 22959890827008, query id 2520890 210.179.37.10 admin updating
update debate set b_id=3,deb_created_date='2023-08-06 09:27:18.439',deb_description='수정했음7',deb_participants=1,deb_pw=null,deb_topic='테스트3',deb_type=0 where deb_id=104
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 41 page no 4 n bits 96 index PRIMARY of table `bstdb`.`debate` trx id 40660 lock mode S locks rec but not gap
Record lock, heap no 23 PHYSICAL RECORD: n_fields 12; compact format; info bits 64
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 41 page no 4 n bits 96 index PRIMARY of table `bstdb`.`debate` trx id 40660 lock_mode X locks rec but not gap waiting
Record lock, heap no 23 PHYSICAL RECORD: n_fields 12; compact format; info bits 64
*** (2) TRANSACTION:
TRANSACTION 40658, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 23826, OS thread handle 22960711501568, query id 2520889 210.179.37.10 admin updating
update debate set b_id=3,deb_created_date='2023-08-06 09:27:18.439',deb_description='수정했음13',deb_participants=1,deb_pw=null,deb_topic='테스트3',deb_type=0 where deb_id=104
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 41 page no 4 n bits 96 index PRIMARY of table `bstdb`.`debate` trx id 40658 lock mode S locks rec but not gap
Record lock, heap no 23 PHYSICAL RECORD: n_fields 12; compact format; info bits 64
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 41 page no 4 n bits 96 index PRIMARY of table `bstdb`.`debate` trx id 40658 lock_mode X locks rec but not gap waiting
Record lock, heap no 23 PHYSICAL RECORD: n_fields 12; compact format; info bits 64
*** WE ROLL BACK TRANSACTION (2)
내가 생각하기론..
tx1 수행된 부분
- insert into debateMember -> fk인 debate(debateId: 104)에 s-lock이 걸린다.
tx2 수행된 부분
- insert into debateMember -> fk인 debate(debateId: 104)에 s-lock이 걸린다.
tx1 수행될 부분
- update debate -> update를 위해서는 x-lock이 필요하다.
tx2 수행될 부분
- update debate -> update를 위해서는 x-lock이 필요하다.
즉, tx1, tx2 모두 s-lock은 획득한 상태에서 x-lock이 필요하기 때문에 데드락이 발생했다.
실제로 수행해보면, 하나의 트랜잭션만 수행되어 저장된 것을 볼 수 있을 것이다.
이에 대해 GPT에게 물어본 답변은 다음과 같다.
mysql에서 두 트랜잭션이 동시에 실행된다고 할 때, 만약 한 트랜잭션이 락을 획득하고 있어 다른 트랜잭션이 락을 획득하지 못한다면 바로 데드락이 발생하나요? 아니면 한 트랜잭션이 락을 반납할 때까지 대기하나요?
MySQL의 InnoDB 스토리지 엔진은 데드락을 방지하고 해결하기 위한 매커니즘을 갖추고 있습니다. 두 트랜잭션이 데드락에 빠지면 MySQL은 이를 감지하고 하나의 트랜잭션을 롤백시켜 데드락을 해제하려고 시도합니다. 이 과정에서 선택된 트랜잭션이 롤백되고 다른 트랜잭션이 진행될 수 있도록 합니다.
따라서 데드락 상황은 MySQL의 스토리지 엔진이 처리하려고 하며, 일반적으로 개발자가 직접 처리할 필요는 없습니다. 그러나 데드락이 자주 발생한다면 트랜잭션의 범위와 로직을 검토하여 데드락이 발생할 가능성을 줄이는 방법을 고려할 수 있습니다.
또한, 두 트랜잭션이 상호 작용하는 방식에 따라 데드락 발생 가능성이 달라질 수 있습니다. 특정 상황에서 데드락이 발생할 수 있기 때문에 트랜잭션을 설계할 때 상호작용과 락 사용을 신중하게 고려하는 것이 중요합니다.
'개발 > Spring' 카테고리의 다른 글
[N+1 문제] EAGER로딩에서 N+1이 발생하지 않는 경우 (0) | 2023.10.03 |
---|---|
[리팩토링] Spring-data-JPA와 Querydsl을 함께 사용하며 (0) | 2023.10.01 |
MySQL 비관적 락을 이용한 인원 제한 구현하기 (0) | 2023.08.17 |
[N+1 문제] 일반조인, fetch조인으로 OSIV, LAZY/EAGER, N+1문제 확실히 이해하기 (0) | 2023.08.16 |
JPA 프록시 개념/존재이유와 find(), getReferenceById() (0) | 2023.08.08 |
댓글