같은 사용자가 동일한 드랍을 반복 조회할 때마다 DB 조회수가 증가해 과집계가 발생하고, DB UPDATE 부하도 높았습니다.
DropsQueryService에서 Redis SETNX로 중복 방문을 선점한 뒤 DB를 증가시킵니다.
📋 처리 흐름
1. 🪪 식별자 생성
- 로그인 사용자 → 이메일
- 비로그인 사용자 → IP + UserAgent 해시
2. 🔑 Redis SETNX: "drop:view:{dropId}:{identifier}" (TTL: 600초)
- ✅ 성공 (첫 방문) → DB 조회수 +1
- ⛔ 실패 (재방문) → 조회수 증가 없음
3. 💥 DB UPDATE 실패 시 → Redis 키 삭제 (보상 로직)
💡
rollbackViewKey보상 로직으로 DB 실패 시 Redis 키를 롤백하여 다음 방문에 재시도가 가능합니다.
과집계 방지 및 불필요한 DB UPDATE 부하 감소
드랍 오픈 시 동시 주문 요청이 몰릴 때 DB 행을 직접 UPDATE하는 방식은 락 경합으로 처리량이 제한되고 초과 차감 위험이 있었습니다.
DropsStockPreemptionService에서 Redis Lua 스크립트로 재고를 먼저 원자 선점한 뒤, 분산락으로 DB에 반영합니다.
⚙️ 재고 선점 스크립트 (RESERVE_SCRIPT)
local current = redis.call('get', KEYS[1])
if not current then return -2 end -- 키 없음 (ACTIVE 전 요청 거부)
current = tonumber(current)
local quantity = tonumber(ARGV[1])
if current < quantity then return -1 end -- 재고 부족
return redis.call('decrby', KEYS[1], quantity)
🔄 보상 스크립트 (INCREASE_IF_EXISTS_SCRIPT)
if redis.call('exists', KEYS[1]) == 1 then
redis.call('incrby', KEYS[1], ARGV[1])
return 1
end
return 0
🗂️ 주요 메서드
| 메서드 | 역할 |
|---|---|
tryReserve | 주문 시 Redis 재고 원자 차감 |
compensateReservedStock | 트랜잭션 롤백 시 Redis 재고 복원 |
increaseStockAfterRestore | 주문 취소 후 재고 복원 |
preloadStockKey | 드랍 ACTIVE 전환 시 Redis에 재고 사전 적재 |
💡
TransactionSynchronizationManager를 통해 트랜잭션 롤백 시 자동으로 보상 로직이 실행됩니다.
동시 요청에 대한 원자 처리로 초과 차감 방지 및 DB 락 경합 제거
드랍 목록 조회 시 연관 상품(Product)을 지연 로딩으로 가져와 드랍 수만큼 추가 쿼리가 발생했습니다.
DropsRepository의 목록 조회 메서드 전반에 @EntityGraph를 적용했습니다.
// DropsRepository.java
@EntityGraph(attributePaths = "product")
Page<Drops> findAllByStatusIn(List<DropsStatus> statuses, Pageable pageable);
💡 드랍 목록 / 판매자별 드랍 / 상품별 드랍 조회 모두 Drops + Product를 단일 쿼리로 처리합니다.
드랍 목록 조회 시 N+1 쿼리 제거
단일 인스턴스 가정 하의 @Scheduled는 멀티 인스턴스 환경에서 모든 인스턴스가 동시에 드랍 상태 전이를 실행해 중복 처리가 발생했습니다.
DropsStatusScheduler에 ShedLock을 적용해 동시에 하나의 인스턴스만 스케줄러를 실행합니다.
@Scheduled(fixedDelayString = "${drops.scheduler.fixed-delay-millis:30000}")
@SchedulerLock(
name = "dropsStatusScheduler_transitionDropsStatus",
lockAtMostFor = "${drops.scheduler.lock-at-most-for:PT1M}",
lockAtLeastFor = "${drops.scheduler.lock-at-least-for:PT1S}")
public void transitionDropsStatus() { ... }
⚙️ 락 설정
| 설정 | 값 | 의미 |
|---|---|---|
⏱️ fixedDelay | 30초 | 실행 주기 |
🔒 lockAtMostFor | 1분 | 인스턴스 장애 시 락 자동 해제 |
🔓 lockAtLeastFor | 1초 | 빠른 종료 후 다른 인스턴스 즉시 실행 방지 |
💡 락은
shedlock테이블(JdbcTemplateLockProvider)로 관리하며, 상태 전이는SCHEDULED → ACTIVE → FINISHED두 단계로 수행됩니다.
멀티 인스턴스 환경에서 드랍 상태 전이 중복 실행 방지
| 항목 | 적용 기술 | 주요 효과 |
|---|---|---|
| 👁️ 조회수 집계 | Redis SETNX + 보상 로직 | 과집계 방지 및 DB UPDATE 부하 감소 |
| 📦 재고 선점 | Lua 스크립트 + 분산락 | 동시 요청 원자 처리 및 초과 차감 방지 |
| 📋 드랍 목록 조회 | @EntityGraph | N+1 쿼리 제거 |
| 🕐 상태 전이 스케줄링 | ShedLock | 멀티 인스턴스 중복 실행 방지 |