본문 바로가기
데이터베이스 & ORM

Row-Level Lock

by DoRightting 2024. 11. 12.

1. Row-Level Lock 개념

Row-Level Lock은 데이터베이스에서 동시성 제어를 위해 개별 행(row) 단위로 잠금을 수행하는 메커니즘

1.1 Lock 종류

-- 1. Exclusive Lock (X-Lock)
SELECT * FROM payments
WHERE payment_id = 'P123' FOR UPDATE;

-- 2. Shared Lock (S-Lock)
SELECT * FROM payments
WHERE payment_id = 'P123' FOR SHARE;

-- 3. 조건부 Lock
SELECT * FROM payments
WHERE payment_id = 'P123' FOR UPDATE SKIP LOCKED;

1.2 JPA에서의 사용

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Payment p WHERE p.paymentId = :paymentId")
Optional<Payment> findByPaymentIdWithLock(@Param("paymentId") String paymentId);

2. 실무 적용 사례

2.1 결제 처리 시스템

@Service
@Transactional
public class PaymentService {

    @Autowired
    private PaymentRepository paymentRepository;

    public void processPayment(String paymentId) {
        // 비관적 락으로 결제 정보 조회
        Payment payment = paymentRepository.findByIdWithPessimisticLock(paymentId)
            .orElseThrow(() -> new PaymentNotFoundException(paymentId));

        // 동시 처리 방지된 상태에서 결제 처리
        if (payment.getStatus() == PaymentStatus.PENDING) {
            payment.setStatus(PaymentStatus.PROCESSING);
            processPaymentLogic(payment);
        }
    }
}

2.2 재고 관리 시스템

@Service
public class InventoryService {

    @Transactional
    public void decreaseStock(Long productId, int quantity) {
        // 비관적 락으로 재고 조회
        Product product = productRepository.findByIdWithPessimisticLock(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        // 재고 차감 로직
        if (product.getStock() >= quantity) {
            product.decreaseStock(quantity);
        } else {
            throw new InsufficientStockException();
        }
    }
}

2.3 Lock Timeout 처리

@Configuration
public class JpaConfig {
    @Bean
    public JpaProperties jpaProperties() {
        JpaProperties props = new JpaProperties();
        props.getProperties().put("javax.persistence.lock.timeout", "3000");
        return props;
    }
}

3. 성능 최적화 전략

3.1 Lock 범위 최소화

// 좋은 예시
@Query("SELECT p FROM Payment p WHERE p.id = :id AND p.status = 'PENDING'")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Payment> findPendingPaymentById(@Param("id") Long id);

// 피해야 할 예시
@Query("SELECT p FROM Payment p WHERE p.status = 'PENDING'")
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Payment> findAllPendingPayments();

3.2 Dead Lock 방지

@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
    // 항상 작은 ID의 계좌를 먼저 잠금
    Long firstLockId = Math.min(fromAccountId, toAccountId);
    Long secondLockId = Math.max(fromAccountId, toAccountId);

    Account firstAccount = accountRepository.findByIdWithLock(firstLockId);
    Account secondAccount = accountRepository.findByIdWithLock(secondLockId);

    // 송금 로직 수행
}

 

Row-Level Lock과 Table-Level Lock의 차이점과 각각의 사용 시나리오

  1. Lock 범위
    • Row-Level Lock: 개별 행 단위로 잠금
    • Table-Level Lock: 테이블 전체를 잠금
  2. 동시성
    • Row-Level Lock: 다른 행에 대한 동시 처리 가능
    • Table-Level Lock: 테이블 전체가 잠기므로 동시성 제한
  3. // Row-Level Lock 예시 @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Payment p WHERE p.id = :id") Optional<Payment> findByIdWithLock(@Param("id") Long id);
  4. 사용 시나리오
    • Row-Level Lock: 결제 처리, 재고 관리 등 개별 레코드 단위의 동시성 제어
    • Table-Level Lock: 스키마 변경, 대량 데이터 업데이트 등

Optimistic Lock과 Pessimistic Lock의 차이점과 선택 기준

  1. 동작 방식
    • Optimistic: 버전 관리를 통한 충돌 감지
    • Pessimistic: 실제 DB 수준의 Lock 획득
  2. // Optimistic Lock @Version private Long version; // Pessimistic Lock @Lock(LockModeType.PESSIMISTIC_WRITE)
  3. 성능 특성
    • Optimistic: Lock 오버헤드 없음, 충돌 시 재시도 필요
    • Pessimistic: Lock 획득 오버헤드, 충돌 방지 보장
  4. 선택 기준
    • Optimistic: 충돌이 적은 경우, 읽기가 많은 경우
    • Pessimistic: 충돌이 빈번한 경우, 데이터 정합성이 중요한 경우

Dead Lock을 방지 전략

  1. Lock 획득 순서 정규화
  2. @Transactional public void processTwoAccounts(Long acc1Id, Long acc2Id) { // 항상 작은 ID부터 Lock 획득 Long firstId = Math.min(acc1Id, acc2Id); Long secondId = Math.max(acc1Id, acc2Id); // Lock 획득 Account first = accountRepo.findByIdWithLock(firstId); Account second = accountRepo.findByIdWithLock(secondId); }
  3. Lock Timeout 설정
  4. spring.jpa.properties.javax.persistence.lock.timeout=5000
  5. Lock 범위 최소화
    • 필요한 레코드만 Lock
    • Transaction 시간 최소화
  6. Dead Lock 감지 및 로깅
    • Dead Lock 모니터링 구현
    • 발생 시 자동 알림

Row-Level Lock 성능 이슈와 해결 방법

  1. Lock 경합 문제
  2. // 해결책: SKIP LOCKED 사용 @Query(value = "SELECT * FROM payments WHERE status = 'PENDING' " + "FOR UPDATE SKIP LOCKED LIMIT 100", nativeQuery = true) List<Payment> findPendingPaymentsForProcessing();
  3. Transaction 시간 최적화
  4. @Transactional public void processPayment(String paymentId) { // 사전 검증 if (!paymentValidator.isValid(paymentId)) { return; } // Lock 획득 후 최소한의 작업만 수행 Payment payment = paymentRepository.findByIdWithLock(paymentId); payment.process(); }
  5. 배치 처리 도입
    • 대량 처리 시 벌크 연산 활용
    • 페이지네이션 적용

Row-Level Lock의 격리 수준(Isolation Level)과의 관계

  1. 격리 수준별 Lock 동작
  2. @Transactional(isolation = Isolation.REPEATABLE_READ) public void processWithLock() { // REPEATABLE_READ에서의 Lock 동작 }
  3. 주요 고려사항
    • READ_COMMITTED: 기본적인 Row-Level Lock 지원
    • REPEATABLE_READ: Range Lock 추가 발생 가능
    • SERIALIZABLE: 가장 엄격한 Lock 적용
  4. 실무 적용
    • 대부분 READ_COMMITTED 사용
    • 특수한 경우만 상위 격리 수준 적용

낙관적 락과 비관적 락을 혼용해서 사용한 경험

  1. 사용 사례
  2. @Entity public class Product { @Version private Long version; // 일반적인 재고 관리용 @Lock(LockModeType.PESSIMISTIC_WRITE) public void criticalUpdate() { // 중요 업데이트용 // 로직 } }
  3. 적용 전략
    • 일반 업데이트: 낙관적 락
    • 결제 처리: 비관적 락
    • 재고 관리: 상황에 따라 선택
  4. 이점
    • 유연한 동시성 제어
    • 성능과 안정성 균형

 

'데이터베이스 & ORM' 카테고리의 다른 글

PostgreSQL & pgvector  (0) 2024.11.16
REPEATABLE READ  (0) 2024.11.15
VACUUM ANALYZE  (1) 2024.11.11
PostgreSQL GIN  (5) 2024.11.10
QueryDSL 개요  (0) 2024.07.25