3/15(일) Redisson 분산락

dev_joo·2026년 3월 15일

Redisson Config

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

@Configuration
public class RedissonConfig {
    @Value("${spring.data.redis.host}")
    private String redisHost;

    @Value("${spring.data.redis.port}")
    private int redisPort;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 단일 Redis 서버 세팅
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
        return Redisson.create(config);
    }
}

Facade

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class RedissonLockOrderFacade {

    private final RedissonClient redissonClient;
    private final OrderService orderService;

    public OrderResponseDto createOrder(OrderRequestDto requestDto) {
        // 1. 락 이름 설정 (상품 ID를 기준으로 락을 잡음)
        RLock lock = redissonClient.getLock("product_lock:" + requestDto.getProductId());

        try {
            // 2. 락 획득 시도 (최대 10초 대기, 1초 동안 락 점유 - 데드락 방지 ; 예상되는 로직 수행 시간보다 충분히 길게 잡거나, Redisson의 Watchdog(락 연장 기능) 기능을 활용)
            boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);

            if (!available) {
                // 락 획득 실패 시 예외 처리 (재시도 로직을 넣기도 함)
                throw new RuntimeException("락 획득 실패: 현재 주문량이 많습니다.");
            }

            // 3. 실제 주문 로직 호출 (여기서 @Transactional이 시작되고 끝남)
            return orderService.createOrder(requestDto);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 4. 트랜잭션 커밋 완료 후 락 해제
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public OrderResponseDto getOrder(Long orderId) {
        return orderService.getOrder(orderId); // 그냥 전달만 함
    }

    public Page<OrderResponseDto> getOrders(int page, int size) {
        return orderService.getOrders(page, size); // 그냥 전달만 함
    }
}

Controller

@RequiredArgsConstructor
@RestController
@RequestMapping("/orders")
public class OrderController {
    // private final OrderService orderService; // 기존 코드 주석 처리
    private final RedissonLockOrderFacade orderFacade; // Facade 주입

    @PostMapping
    public ResponseEntity<OrderResponseDto> createOrder(@RequestBody OrderRequestDto requestDto) {
        OrderResponseDto dto = orderFacade.createOrder(requestDto);
        return ResponseEntity
                .created(URI.create("/orders/" + dto.getOrderId()))
                .body(dto);
    }

    @GetMapping("{orderId}")
    public ResponseEntity<OrderResponseDto> getOrder(@PathVariable Long orderId) {
        OrderResponseDto dto = orderFacade.getOrder(orderId);
        return ResponseEntity.ok(dto);
    }

    @GetMapping
    public ResponseEntity<Page<OrderResponseDto>> getOrders(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size
    ) {
        Page<OrderResponseDto> dtos = orderFacade.getOrders(page, size);
        return ResponseEntity.ok(dtos);
    }

}

Postman "Runner"로 동시성 테스트하기

Postman에는 요청을 여러 번 동시에 쏘는 기능이 있다.

Postman 왼쪽 상단의 [Collections] 탭에서 API가 담긴 폴더를 클릭
우측 상단의 [Run] 버튼

  • Iterations: 10~20 (요청 횟수)
  • Delay: 0ms (딜레이 없음)

    (분산락을 제대로 테스트 하려면, 다른 포트에서도 서버를 띄워 여러 개의 포트로 동시에 요청 한다.)

테스트 조건

요청 횟수보다 적은 수량으로 개수를 준비해준다.

요청의 Body를 해당 상품을 주문하도록 제품 ID와 수량을 설정해 저장해준다.

Redis(docker) 확인

docker exec -it my-redis redis-cli
monitor

Docker Desktop 확인

redis-cli


실행 결과 터미널에 SET, EXPIRE, DEL 같은 명령어들이 주르륵 뜨는 것으로 Redisson이 락을 걸고 있음을 확인할 수 있다.

트러블 슈팅 - 락은 실행 됐는데 결과가 예상과 다름

200응답과 함께 주문은 생성되었다.
그런데 기가막히게 '한정수량' 빵의 수량은 줄지 않았다.

기존 RedissonLock 수정하기

@Component
@RequiredArgsConstructor
public class RedissonLockOrderFacade {

    public OrderResponseDto createOrder(OrderRequestDto requestDto) {
			...
        try {
            // 2. 락 획득 시도 (최대 10초 대기, 1초 동안 락 점유 - 데드락 방지 ; 예상되는 로직 수행 시간보다 충분히 길게 잡거나, Redisson의 Watchdog(락 연장 기능) 기능을 활용)
            boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);

          ...

원인은 데드락 방지를 위해 1초로 설정한 락 적용 시간이 너무 짧아
다음 요청이 들어오기 전 락이 풀려버린 것 같다.
(로직 수행 중에 락이 해제되어, 다른 스레드가 아직 업데이트되지 않은 DB의 재고 데이터를 읽게 되는 Race Condition이 발생)

WatchDog기능 활성으로 락 자동으로 연장

// leaseTime을 -1로 설정하여 로직 종료 시까지 락 유지
boolean available = lock.tryLock(10, -1, TimeUnit.SECONDS);


덕심으로 이겨내자! (플레이브 4월 13일 컴백 많관부)



정상적인 수량으로 완판되었다.🥰

profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글