본문 바로가기
개발/Spring

MySQL 외래키와 데드락

by meanjung 2023. 8. 17.

엔티티 구조

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 수행된 부분

  1. insert into debateMember -> fk인 debate(debateId: 104)에 s-lock이 걸린다. 

tx2 수행된 부분

  1. insert into debateMember -> fk인 debate(debateId: 104)에 s-lock이 걸린다.

tx1 수행될 부분

  1. update debate -> update를 위해서는 x-lock이 필요하다.

tx2 수행될 부분

  1. update debate -> update를 위해서는 x-lock이 필요하다.

즉, tx1, tx2 모두 s-lock은 획득한 상태에서 x-lock이 필요하기 때문에 데드락이 발생했다. 

 

 

실제로 수행해보면, 하나의 트랜잭션만 수행되어 저장된 것을 볼 수 있을 것이다.

 

이에 대해 GPT에게 물어본 답변은 다음과 같다. 

 

mysql에서 트랜잭션이 동시에 실행된다고 , 만약 트랜잭션이 락을 획득하고 있어 다른 트랜잭션이 락을 획득하지 못한다면 바로 데드락이 발생하나요? 아니면 트랜잭션이 락을 반납할 때까지 대기하나요?

 
MySQL에서 두 개의 트랜잭션이 동시에 실행되고 한 트랜잭션이 락을 획득하고 있는 상황에서 다른 트랜잭션이 락을 획득하지 못한다면, 바로 데드락이 발생하는 것은 아닙니다. MySQL은 일반적으로 데드락을 자동으로 감지하고 처리하려고 시도합니다.

MySQL의 InnoDB 스토리지 엔진은 데드락을 방지하고 해결하기 위한 매커니즘을 갖추고 있습니다. 두 트랜잭션이 데드락에 빠지면 MySQL은 이를 감지하고 하나의 트랜잭션을 롤백시켜 데드락을 해제하려고 시도합니다. 이 과정에서 선택된 트랜잭션이 롤백되고 다른 트랜잭션이 진행될 수 있도록 합니다.

따라서 데드락 상황은 MySQL의 스토리지 엔진이 처리하려고 하며, 일반적으로 개발자가 직접 처리할 필요는 없습니다. 그러나 데드락이 자주 발생한다면 트랜잭션의 범위와 로직을 검토하여 데드락이 발생할 가능성을 줄이는 방법을 고려할 수 있습니다.

또한, 트랜잭션이 상호 작용하는 방식에 따라 데드락 발생 가능성이 달라질 있습니다. 특정 상황에서 데드락이 발생할 있기 때문에 트랜잭션을 설계할 상호작용과 사용을 신중하게 고려하는 것이 중요합니다.

댓글