이번 포스팅에선 네임드 락을 이용해 데드락을 해결하는 과정을 담았습니다.
이전 포스팅이 궁금하시다면 아래 링크를 참고해 주세요!
2024.11.25 - [Project] - [포텐데이 409-1pick] 분석 서비스 개발 중 만난 데드락 해결기 (1)
2024.11.26 - [Project] - [포텐데이 409-1pick] 분석 서비스 개발 중 만난 데드락 해결기 (2)
네임드 락에 대해
MySQL 엔진 레벨에서 제공하는 네임드 락은 임의의 문자열에 대해 잠금을 설정할 수 있습니다.
핵심은 잠금의 대상이 테이블이나 레코드같은 데이터베이스 객체가 아니라 단순히 지정한 문자열에 대해 `획득(Acquire)`과 `반납(Release)`하는 잠금입니다.
사용 방법
잠금 획득
-- 'lockName'에 대해 잠금을 획득, 만약 잠금이 사용 중이면 5초를 대기 (5초 이후 대기 해제)
SELECT GET_LOCK('lockName', 5);
잠금을 획득하면 1, 이미 잠금이 있으면 지정한 시간(초)만큼 대기합니다.
만약 대기 시간동안 잠금을 획득하지 못하면 0을 반환합니다.
잠금 확인
-- 잠금이 설정되어있지 않으면 1, 이미 잠금이 있으면 0 반환
SELECT IS_FREE_LOCK('lockName');
해당 문자열에 대해 잠금이 없으면 1, 잠금이 있으면 0을 반환합니다.
잠금 해제
-- 잠금 해제 성공 시 1, 그렇지 않으면 0을 반환
SELECT RELEASE_LOCK('lockName');
잠금 해제 성공 시 1, 실패 시 0을 반환합니다.
네임드 락 구현하기
JPA에서 `@Lock` 애너테이션은 낙관적 락과 비관적 락만 지원합니다.
따라서 네임드 락은 별도로 구현해야 합니다.
NamedLockRepository 구현
@Slf4j
@Repository
public class NamedLockRepository {
@PersistenceContext
private EntityManager em;
@Transactional(propagation = Propagation.MANDATORY)
public boolean acquireLock(String name, int timeoutSeconds) {
String sql = "SELECT GET_LOCK(:name, :timeoutSeconds)";
Object result = em.createNativeQuery(sql)
.setParameter("name", name)
.setParameter("timeoutSeconds", timeoutSeconds)
.getSingleResult();
log.info("[Acquire Lock] Thread Name={}, result={}", Thread.currentThread().getName(), result);
return result != null && result.toString().equals("1");
}
@Transactional(propagation = Propagation.MANDATORY)
public boolean releaseLock(String name) {
String sql = "SELECT RELEASE_LOCK(:name)";
Object result = em.createNativeQuery(sql)
.setParameter("name", name)
.getSingleResult();
log.info("[Release Lock] Thread Name={}", Thread.currentThread().getName());
return result != null && result.toString().equals("1");
}
}
- `EntityManager`를 주입받고, nativeQuery를 통해 네임드 락을 제어하도록 설계했습니다.
- `@Transactional(propagation = Propagation.MANDOTORY)`: 구현하는 네임드 락은 동시성 제어를 목적으로 사용되고, 트랜잭션 일관성을 깨뜨리지 않아야 합니다. 따라서 락을 획득하고 반납하는 곳에 트랜잭션 전파 속성을 `MANDATORY`로 설정했습니다.
`MANDOTORY` 속성은 기존 트랜잭션이 존재하지 않으면 `IllegalTransactionStateException` 예외가 발생합니다.
호출하는 쪽에서 반드시 트랜잭션을 열어야하는 제약을 만들기 때문에 개발자가 실수할 확률을 줄여줄 수 있습니다.
네임드 락 적용으로 데드락 해결하기
네임드 락 적용
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class DailyReportService {
// 기존 코드 생략..
// 네임드 락 Repository 추가
private final NamedLockRepository namedLockRepository;
public DailyReportResponseDto createDailyReport(DailyReportDto.CreateRequest dailyReportDto) {
// 네임드 락 획득
String lockName = "createDailyReport";
boolean lockAcquired = namedLockRepository.acquireLock(lockName, 2);
// 네임드 락 획득 실패 시 예외 처리 (409 Conflict)
if (!lockAcquired) {
throw new NamedLockAcquisitionException("Failed to acquire lock:" + Thread.currentThread().getName());
}
// 네임드 락 획득 시 기존 로직 처리
try {
// 기존 코드 생략..
} finally {
// 네임드 락 반납
boolean released = namedLockRepository.releaseLock(lockName);
// 반납 실패 시 경고 로그 생성
if (!released) {
log.warn("Failed to release lock: {}", lockName);
}
}
}
- 락 획득에 실패하면 커스텀 예외인 `NamedLockAcquisitionException`를 던집니다.
이는 `RestControllerAdvice`에서 `409 Conflict`로 처리합니다.
- 락 획득에 성공하면 `try-finally` 안에서 정상 로직을 처리합니다.
만약 데이터베이스 서버 오류로 락 반납에 실패하면 경고 로그를 생성합니다.
테스트 결과
테스트는 모두 통과했습니다.
100개의 스레드에서 1개를 제외한 나머지 스레드는 예외를 발생시켰는데, 네임드 락을 획득하지 못한 예외와 데일리 리포트가 존재해서 발생하는 예외였습니다.
또 다른 문제
하지만 또 다른 문제가 생겼습니다. 데일리 리포트에 대해 `INSERT` 쿼리가 2번 나가는 문제입니다.
데일리 리포트는 1번만 생성되야하는데, 2번 생성되는 문제가 발생했습니다.
다음 포스팅에서 이 문제를 해결해보겠습니다.
다음 포스팅:
2024.11.27 - [Project] - [포텐데이 409-1pick] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (4)
긴 글 읽어주셔서 감사합니다.
'Project' 카테고리의 다른 글
올려 올려 라디오 NCloud 활용 후기 (2) | 2024.11.27 |
---|---|
[포텐데이 409-1pick] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (4) (2) | 2024.11.27 |
[포텐데이 409-1pick] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (2) (0) | 2024.11.26 |
[포텐데이 409-1pick] 분석 서비스 개발 중 만난 데드락과 동시성 문제 해결기 (1) (0) | 2024.11.25 |
[포텐데이 409-1pick] 올려올려 라디오 서비스 개발기 - 2 (2) | 2024.10.08 |