개발

트랜잭션 데드락 - 외래키 제약조건

용준한 2025. 4. 6. 23:32

문제

예측되는 상황 :

 

같은 게시글에 대해 동시에 좋아요를 하는 테스트시, 트랜잭션에서 데드락이 발생하였다.

문제의 원인 파악하기

해당 로직에서 사용되는 쿼리를 순서대로 적용해가며 단계별로 테이블에서 획득한 락을 분석하여 외래키 제약조건으로 인한 공유락이 원인임을 파악했다.

해당 플랫폼의 요구사항 고려해보기

  • 유사한 서비스(ex 에브리타임)을 보았을 때, 비인기 게시글에 대한 좋아요의 동시접근의 확률은 낮아 보인다.
  • 인기 게시글은 Redis에 캐싱되어있는 상태이다. 
  • 구동 환경 : MySQL 8.0 (innoDB)이며, 트랜잭션 격리 레벨은 디폴트 값인 Repeatable Read
  • 로직 : 좋아요 대상 게시글 조회 → 이미 좋아요 했는지 조회 → 좋아요-게시글 테이블에 추가 → 게시글의 좋아요 수 업데이트

 

 

해결 방안 및 선택 이유 :

전체적인 구조로 보았을 때, 이 문제를 해결하는 방법은 외래키 자체를 없애거나, DB에서 락으로 해결을 하거나, 애플리케이션 단에서 해결하는 방법이다.

 

외래키를 없애기

 

DB에서 해결

DB단에서 이 문제를 해결한다면 결국 각각의 MySQL 클라이언트 스레드가 락을 대기하게 된다. 이는 곧 컨넥션 점유, 즉 리소스 소모로 이어진다. 따라서 애플리케이션 단에서 해결할 필요가 있다.

 

애플리케이션에서 해결

서버 애플리케이션에서 해결을 하려면 여러 방법이 있다. 메소드 자체에 synchronized 키워드를 사용하거나 (서로 다른 데이터에 대한 접근끼리도 대기를 해야한다는 단점이 존재), HashMap에서 데이터 아이디를 키로 갖고 동시성 제어를 하는 방법을 고안했으나, 

서버를 확장(scale-out)한다고 했을 때 결국 디비에서 다시 락을 관리해야한다.

 

결국에는 중앙적으로 락을 관리하는 것이 필요한데 이것이 레디스의 락을 사용한 이유다. 또한 기존에 레디스 인프라를 재사용할 수 있다는 강점이 존재했다.

Redis

레디스에서 자바로 분산락은, 두가지 클라이언트로 구현할 수 있다.

 

Lettuce : 분산락을 위한 인터페이스를 제공하지는 않는다. 따라서 spin-lock 방식으로 구현을 해야한다. 글로벌 레디스 캐시 서버로 spin-lock 방식으로 요청을 보내면 네트워크 I/O 부하가 있을 것이라고 판단하였다

 

Redisson : pub/sub 방식의 락 인터페이스를 제공한다. 기본으로 제공되는 클라이언트 라이브러리가 아니라  관리해야 하는 커넥션이 더 생기겠으나, 이미 기존에 인기 게시글에서 다양한 자료구조를 위해 사용중이다.

 

하나의 레디스에서, 이미 다른 서비스(인기 게시글 관련)를 제공하고 있어 서비스간 영향이 있겠으나, 아직 분리를 할정도로 서비스에 영향이 있지는 않은것 같다. 

 

결국 하나의 레디스에 부하를 줄이기 위해 redisson 방식을 채택했다.

 

만약에 사용자가 좋아요 요청을 보내고 응답을 얼마나 기다릴까?

무한정 기다리지는 않을 것이다. 많아봐야 1초? 

사용자는 다시 좋아요 버튼을 누를 것이고 그때 다시 처리 해주면 된다.

즉 이전의 요청은 락을 오랫동안 기다리면서 다른 요청들에게 지연을 유발할 필요가 없다. 따라서 락을 1초동안 획득 못할시, 예외 처리를 해주었다.

또한 매우 간단한 로직이기에 길게 락 획득 시간을 부여할 필요가 없어서 일단 1초로 했는데 이부분에 관해서는 더 생각을 해봐야 할 것 같다.

RLock lock = redissonClient.getLock("plan:likes:" + planId);
try {
    if (!lock.tryLock(1L, 1L, TimeUnit.SECONDS))
        throw new RuntimeException("락 획득 실패");
    planRepository.likePlan(userId,planId);
    planRepository.upLike(planId);

} catch (Exception e) {
    throw e;
} finally {
    if (lock != null && lock.isLocked())
        lock.unlock();
}

 

서로 다른 게시글 ID에 대한 요청끼리 락 경합을 할 필요가 없으므로 각가의 게시글 ID 별로 락을 관리하도록 하였다.

 

다음글

https://loftspace.tistory.com/48

 

트랜잭션 데드락 - 고민

이전에 포스팅에서 트랜잭션 데드락을 여러 이유로 레디스를 활용하여 해결하였다.예외 상황에 대해서 고민을 해보았다. 레디스 서버가 다운 된다면만약에 레디스 서버가 다운된다면 락 기능

loftspace.tistory.com