[재고 관리 #3] 트러블슈팅: Redis를 활용하여 DB 데드락 해결하기

RDBMS의 트랜잭션과 락으로 인한 데드락을 Redis 기술을 활용하여 해결한 사례를 소개합니다.

🌱 Overview

이전 글(구조적 변경을 통해 이슈 해결하기)에서 다뤘던 DB 데드락을 구조적 변경 대신에 기술적으로 어떻게 풀 것인지 고민해보았습니다.

아무래도 구조나 기획을 변경하게 되면 기존의 계획과는 다른 방향을 가지고 서비스가 완성될 여지가 있기 때문에 기술적으로도 어떠한 이슈를 해결할 수 있는 역량을 쌓아야합니다.

동시적으로 DB의 상품 Table에 접근해서 락을 거는 행위가 데드락을 발생시키는 근본적인 원인이라 생각하여, 해당 프로세스를 어떻게 해결할 것인지를 고민하였습니다.


🌱 Redis로 재고 관리하기

Redis는 다방면에서 활용할 수 있는 정말 유용한 제품이라고 생각합니다. Redis를 통한 분산락, Scale out 고려시 JWT의 토큰이나 Session을 저장하는 스토리지, Geo API 지원을 활용한 위치 데이터 가공 등 많은 상황에서 사용될 수 있습니다.

현재 상황에서는 Redis을 통해 재고관리를 해보고자 합니다.

❗️ Redis 재고관리의 주의사항

Redis는 In-Memory 기반의 스토리지이기 때문에 메인 메모리에 데이터를 저장합니다. 이는 곧 Redis에 저장하는 데이터는 휘발된다는 특징이 있기 때문에 지속성 측면에서 고려해야할 사항들이 많습니다. (물론 메인메모리에 접근하기 때문에 접근 및 저장속도는 월등하게 빠릅니다.)

만일 Redis 서버에 장애가 발생한다면, Redis에 있는 데이터는 모조리 삭제되어 복구가 불가능해집니다. 이에 대비하여 Redis 서버를 Cluster로 구성하여 SPOF의 상황에 대처해야합니다.


🟤 왜 Redis일까요?

Redis는 싱글 스레드 기반으로 동작하는 In-Memory 스토리지입니다. 이 의미는 조회하여 값을 수정하는 단계만 원자적(Atomic)으로 처리가 가능하다면 데이터 정합성에 대한 이슈를 해결할 수 있습니다.

기존에 MySQL을 통한 데이터 수정시 데이터의 정합성이 깨지는 이유는 다음과 같습니다.

  1. SQL 요청은 병렬적으로 처리됩니다.
  2. threadA가 a라는 데이터를 조회하여 수정하려는 찰나 threadB가 a라는 데이터를 조회합니다.
  3. threadA가 처리한 a를 DB에 반영합니다.
  4. 하지만 threadB가 처리한 a를 다시 DB에 반영할 때는 3번에서 threadA가 처리한 데이터를 기반으로 처리하는 것이 아니라 2번에서 조회한 a 데이터를 기반으로 처리하기 때문에 3번으로 인한 데이터 반영은 처리되지 않은 것처럼 됩니다.
  5. 데이터 처리가 오류를 가지고 오게 됩니다.

Redis에는 병렬적으로 데이터를 하더라도 데이터를 조회하여 수정하는 과정을 원자적으로 처리하는 API가 있습니다. 싱글 스레드 기반으로 원자적으로 처리된 데이터를 조회한다면, threadA가 조회하여 처리한 데이터를 threadB가 바로 조회할 수 있기 때문에 데이터 정합성을 보장할 수 있습니다.

stock-management7

❗️ Redis의 트랜잭션

재고를 처리할 때 현재 있는 재고가 0이라면 주문을 할 수 없습니다. 이 경우 MySQL이라면 트랜잭션을 활용하여 데이터를 롤백할 수 있지만, Redis에서의 트랜잭션은 롤백 기능을 지원하지 않습니다.

그렇기 때문에 처리 중 예외 상황이 발생했을 경우를 대비하여 다시 재고를 원상태로 돌려놓는 보상 트랜잭션을 별도로 구현하였습니다.


🌱 구현된 코드

public Long completeOrder(Long orderId, Long employeeId) {
    // Redis 재고에서 먼저 에러 발생 시 뒤의 MySQL 에 쿼리 요청하는 작업에 접근하지 않습니다.
    List<OrderDetail> findOrderDetails = orderDetailRepository.findByOrderId(orderId);

	// Redis에 저장되어 있는 상품 재고 처리
    redisItemStockRepository.decreaseItemStock(findOrderDetails);

	// 재고 처리 완료 update
    Order findOrder = orderRepository.findById(orderId);
    orderRepository.update(orderId, findOrder.toUpdateOrderWhenComplete(OrderStatus.COMPLETE, employeeId));
	
    ...

    return orderId;
}

구현된 코드는 의외로 단순합니다.

  1. orderDetailRepository에서 주문된 아이디에 해당하는 상품과 상품 개수를 조회합니다.
  2. 조회한 상품 리스트에서 decreaseItemStock이라는 메소드를 통해 재고를 감소시킵니다.
  3. decreaseItemStock 메소드가 성공하면 주문 아이디의 상태값을 COMPLETE로 변경시킵니다.

아래는 decreaseItemStock 메소드에 관련된 코드입니다.

public void decreaseItemStock(List<OrderDetail> orderDetails) {
	// Redis의 상품 처리 중 예외 발생시 롤백할 자료구조
    Map<String, Integer> backupItem = new HashMap<>();

	// 상품 리스트를 순환하면서 상품 재고 차감
    for (OrderDetail orderDetail : orderDetails) {
		
		// 상품에 해당하는 KEY 생성
        String generatedKey = generateItemKey(orderDetail.getItemId());

		// 상품 재고 차감
        Long decrement = redisItemStockTemplate.opsForValue().decrement(generatedKey, orderDetail.getCount());
		
		// 상품 재고 차감된 이후 롤백 자료구조에 처리한 상품 정보 캐싱
        backupItem.put(generatedKey, orderDetail.getCount());

		// 상품 재고 처리 확인
        backupItemStock(backupItem, decrement);
    }
}
//----------
private void backupItemStock(Map<String, Integer> backupItem, Long decrement) {
    if (decrement == null || decrement < 0) {
        for (String itemKey : backupItem.keySet()) {
            redisItemStockTemplate.opsForValue().increment(itemKey, backupItem.get(itemKey));
        }

        throw new NotEnoughStockException("재고가 부족합니다.");
    }
}

decreaseItemStock 메소드는 다음과 같은 Flow를 가집니다.

  1. 먼저 처리할 상품 아이디(key), 차감할 상품 개수(value)로 예외가 발생시 롤백할 자료구조를 생성합니다.
  2. redisItemStockTemplate.opsForValue().decrement() 메소드를 사용하여 원자적으로 redis의 value를 조회하고 차감합니다.
  3. 만약 차감된 상품 개수가 음수가 된다면 상품이 모자라다는 의미이기 때문에 이전에 처리하고 백업한 상품들을 롤백합니다. (increment를 통해 롤백합니다.)

이러한 과정을 통해 데이터의 정합성을 지키면서 상품을 데드락 없이 재고 처리할 수 있게 하였습니다.


🌱 마무리

보통 Redis는 In-Memory 기반의 스토리지이기 때문에 유실되지 않아야할 데이터는 가급적 사용하지 않습니다. 상품의 재고는 중요한 자원이라고 생각하여 무조건 Redis를 사용하지 않아야하는 것 아닌가? 라는 의문이 들었지만, 이미 유명 기업에서 Redis를 활용한 재고 관리 사례를 접하게 되어 생각을 전환하는 계기가 되었습니다.


📚 참고 레퍼런스

우아한 기술 블로그 - 선물하기 시스템의 상품 재고는 어떻게 관리되어질까?


© 2021. All rights reserved.