비관적 락으로 동시성 문제를 해결해보자!

홍지범·2023년 7월 3일
1

Intro

낙관적 락은 트랜잭션간 충돌이 발생할 때 데드락을 발생시킬 수 있습니다.
하지만 자바의 멀티 스레드의 특성 때문에 일어나는 일인지는 잘 모르겠네요.
왜냐면 레디스의 락(Watch)는 낙관락 락을 사용하거든요.

이 부분은 나중에 알아보고 비관적 락을 통해 동시성 문제를 해결해보겠습니다.

비관적 락(Pessimistic Lock)

비관적 락은 트랜잭션 충돌이 일어날거라 가정하고 직접 데이터베이스 락을 사용하는 방법 입니다.

다시 격리 수준을 REPEATABLE READ(InnoDB 디폴트)으로 설정하고 직접 락을 걸어 동시성 문제를 해결 해보겠습니다.

InnoDB에서 지원하는 락 종류

InnoDB는 레코드 락(Row-level lock)을 지원하기 때문에 직접 S lock 또는 X lock을 걸 수 있습니다.

  • S lock(Shared Lock) : 특정 Row를 읽을 때 사용하는 락으로 S lock은 트랜잭션들이 동시에 걸 수 있다.
    하지만 S lock이 걸려있는 Row에 X lock을 걸 수 없다.
SELECT ~ LOCK IN SHARE MODE
  • X lock(Exclusive Lock) : 특정 Row에 쓰기를 할 때 사용하는 락으로 특정 Row에 X lock이 걸려있다면 다른 트랜잭션은 읽기(S lock), 쓰기(X lock) 모두 할 수 없다.
SELECT ~ FOR UPDATE
UPDATE ~
DELETE ~

비관적 락 적용

비관적 락을 거는 방법은 특정 트랜잭션을 시작할 때
특정 Row를 조회하며 SELECT ~ FOR UPDATE로 Row lock을 거는 것 입니다.

X lock 은 락을 건 후 독점하기 때문에 변경이 발생하는 Row에만 거는 것이 좋습니다.

    @Transactional
    public void purchase(PurchaseRequest purchaseRequest) {
        Product product = productDao.findById(purchaseRequest.getProductId());
        //상품을 조회할 때 X lock을 겁니다.

		...
        Product updateProduct = product.addPurchasedStock(purchaseRequest.getAmount());
		...
    }

현재 프로젝트는 Mybatis를 사용하고 있으므로 직접 쿼리를 수정해주어야 합니다.

        SELECT
        	...
            totalStock,
            purchasedStock,
            ...
        FROM PRODUCT
        WHERE
            productId = #{productId}
        FOR UPDATE

락 해제

InnoDB에서 트랜잭션 실행 후 건 락은 commit, rollback되었을 때 자동으로 unlock이 됩니다.

동시성 문제가 해결되었는지 확인

  • 테스트는 ngrinder로 10,000 건을 구매하는 테스트를 했습니다.


  • 총 구매이력 : 10,000건
  • 구매된 수량 : 10,000건 (4998 + 5002)

해치웠나..!

동시성 문제를 발견 후 읽고 -> 쓰기 의 연산을 원자적으로 만들어주기 위해 변화가 많은 필드의 읽기 연산에 Row lock을 걸어주었습니다.
때문에 트랜잭션이 끝날 때 까지 락을 건 트랜잭션만이 atomic한 연산을 수행할 수 있었습니다.

이대로 끝난걸까요?


절대 하면 안되는 대사

남은 문제

DB에서 지원하는 락을 통해 동시성 문제를 해결했지만 아직까지 남은 문제가 있습니다.

  • Row에 X lock이 걸려있다면 다른 S lock, X lock을 걸 수 없습니다. 무분별한 for update는 심각한 동시성을 저해합니다.
  • 제어권이 DB로 넘어간 상태이기 때문에 락에 대한 제어가 힘듭니다.

지금은 단순하게 구매 API에 트랜잭션 경합을 피하기 위해 Row lock을 사용했습니다.
하지만 X lock이 걸린 Row는 다른 Row lock을 걸 수 없습니다.
때문에 10분간만 유지되어야 하는 타임딜 서비스가 종료 뒤에도 구매가 이루어지는 문제가 발생할 수 있습니다.

또한, 제어권이 DB로 넘어간 상태라 락 해제는 commit 혹은 rollback이 될 때 까지 제어할 수 없는 상태 입니다.

이렇게 DB에 복잡한 연산을 시킨다던가, 리소스를 많이 잡고 있는 상황은 지양해야 합니다.
DB는 눈에 보이지 않지만 이미 많은 연산(옵티마이저 처럼)을 수행하고 있기 때문에 최대한 가볍게 하는 것이 좋습니다.

  • NCloud에서 모니터링한 DB CPU 사용률
    구매 API만 실행했음에도 사용률이 100%에 임박


  • ngrinder에서 확인한 TPS
    10,000만으로는 금방 끝나기 때문에 더 많은 수를 늘려 실행했습니다. 갈수록 낮아지는 TPS를 확인할 수 있습니다.

결론

  • 구매 API만 실행했음에도 시간이 지날수록 DB 서버의 CPU 사용률이 100%에 임박하고 TPS도 갈수록 낮아졌습니다.
    실제 서비스라면 여러 API 들이 다양하게 발생할 것이며, 이 떄 이벤트 트래픽이 지속될수록 서비스 이용자들은 안좋은 경험을 하게 될 것입니다.

    서버라면 물리적인 대수를 늘려 사용률을 낮출 수 있지만(Scale-Out) DB를 확장하는 행위는 과도한 Trade-Off가 될 수 있습니다.
    지금은 DB가 일종의 병목지점이 되었으며, 이를 다른 지점으로 옮겨 DB의 사용률을 낮출 수 있는 방법이 필요합니다.

요약

  • 동시성을 제어하기 위해 MySQL InnoDB의 X lock을 이용해 원자적 연산을 성공했다.
  • Row에 X lock을 잡고 있기 때문에 동시성 저하의 우려, DB 연산에 부담을 주는 문제를 해결해야한다.
profile
왜? 다음 어떻게?

0개의 댓글