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);
}
}
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); // 그냥 전달만 함
}
}
@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에는 요청을 여러 번 동시에 쏘는 기능이 있다.
Postman 왼쪽 상단의 [Collections] 탭에서 API가 담긴 폴더를 클릭
우측 상단의 [Run] 버튼

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

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

docker exec -it my-redis redis-cli
monitor
redis-cli


실행 결과 터미널에 SET, EXPIRE, DEL 같은 명령어들이 주르륵 뜨는 것으로 Redisson이 락을 걸고 있음을 확인할 수 있다.
200응답과 함께 주문은 생성되었다.
그런데 기가막히게 '한정수량' 빵의 수량은 줄지 않았다.

@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이 발생)
// leaseTime을 -1로 설정하여 로직 종료 시까지 락 유지
boolean available = lock.tryLock(10, -1, TimeUnit.SECONDS);

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




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