📌 개요
현재 enqueue 와 delete 가 다단계 처리되어 race condition 윈도우가 존재한다.
enqueue
- 1단계: setIfAbsent (역인덱스 락)
- 2단계: MULTI/EXEC (Sorted Set + Hash)
- 1단계 성공 후 2단계 실패 시 orphan 역인덱스 가능 (TTL 로 자동 정리되지만 일시적 불일치)
delete
- Java 단 compare-and-delete 적용 (현재):
String current = redisTemplate.opsForValue().get(userProgramKey);
if (tokenIdStr.equals(current)) {
redisTemplate.delete(userProgramKey);
}
- GET 과 DEL 이 별도 명령 → 사이의 race 윈도우 (1~2ms) 존재
- 그 사이 역인덱스 변경 시 다른 토큰의 역인덱스를 잘못 삭제 가능 (Critical)
🎯 목표
🧩 리팩토링 범위
🏗️ 변경 설계
변경 전 구조
enqueue
setIfAbsent (1단계) → MULTI/EXEC (2단계)
→ 1단계 성공 + 2단계 실패 시 orphan 역인덱스
delete
GET → 검사 → DEL (Java 코드)
→ GET 과 DEL 사이 race 가능
변경 후 구조 — Lua Script
enqueue.lua
local userProgramKey = KEYS[1]
local programKey = KEYS[2]
local tokenKey = KEYS[3]
local tokenId = ARGV[1]
local score = ARGV[2]
local ttl = tonumber(ARGV[3])
if redis.call('EXISTS', userProgramKey) == 1 then
return {err = "DUPLICATE"}
end
redis.call('SET', userProgramKey, tokenId, 'EX', ttl)
redis.call('ZADD', programKey, score, tokenId)
redis.call('HMSET', tokenKey, ...)
redis.call('EXPIRE', tokenKey, ttl)
return {ok = "OK"}
delete.lua (compare-and-delete)
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('DEL', KEYS[1])
end
redis.call('ZREM', KEYS[2], ARGV[1])
redis.call('DEL', KEYS[3])
영향 받는 기능
enqueue() — Lua Script 로 통합
delete() — Lua Script 로 통합
- 기존 setIfAbsent + MULTI/EXEC 로직 제거
⚠️ 주의 사항
- 기능 동작은 기존과 동일 (원자성 강화만)
- API 스펙 변경 없음
- DuplicateTokenException 던지는 방식 동일 (Lua return 값으로 판별)
- Lua Script 파일은
src/main/resources/scripts/ 등에 분리 관리
🧪 검증 방법
- 기존 테스트 통과 여부 확인
- 동시성 테스트 추가 (concurrent enqueue, race delete)
- Lua Script 단위 테스트 (Testcontainers Redis 활용)
🔗 참고
📌 개요
현재
enqueue와delete가 다단계 처리되어 race condition 윈도우가 존재한다.enqueue
delete
🎯 목표
🧩 리팩토링 범위
🏗️ 변경 설계
변경 전 구조
enqueue
delete
변경 후 구조 — Lua Script
enqueue.lua
delete.lua (compare-and-delete)
영향 받는 기능
enqueue()— Lua Script 로 통합delete()— Lua Script 로 통합src/main/resources/scripts/등에 분리 관리🧪 검증 방법
🔗 참고