deleteAllByProgramScript
+ ) {
+ this.redisTemplate = redisTemplate;
+ this.properties = properties;
+ this.enqueueScript = enqueueScript;
+ this.deleteScript = deleteScript;
+ this.deleteAllByProgramScript = deleteAllByProgramScript;
+ }
/**
- * Redis 기반 enqueue 구현.
+ * Redis 기반 enqueue 구현 (Lua 스크립트로 원자 처리).
*
- * 2단계 처리:
+ *
Lua 스크립트 안에서 한 트랜잭션으로:
*
- * - 역인덱스(setIfAbsent)를 락처럼 사용하여 중복 진입 방지
- * - Sorted Set + Hash 를 MULTI/EXEC 트랜잭션으로 저장
+ * - 역인덱스 SETNX 로 중복 진입 방지
+ * - INCR 시퀀스 + epoch_second 로 tie-breaker score 생성
+ * - Sorted Set 추가 (ZADD)
+ * - Hash 메타 저장 (HSET) + TTL (EXPIRE)
+ * - 프로그램 단위 토큰 인덱스 (Set) 추가 (SADD)
*
- *
- * 한계: 1단계 성공 후 2단계 실패 시 orphan 역인덱스가 남을 수 있다.
- * TTL 로 자동 정리되지만, 진짜 원자성을 위해 향후 Lua Script 도입 예정.
- *
- *
Sorted Set 의 멤버는 키 단위 TTL 불가하므로,
- * {@link #delete(QueueToken)} 호출 시 명시적 ZREM 으로 정리한다.
*/
@Override
public void enqueue(QueueToken token) {
-
- // 키 생성
String programKey = programKey(token.getProgramId());
String tokenKey = tokenKey(token.getId());
String userProgramKey = userProgramKey(token.getUserId(), token.getProgramId());
+ String seqKey = seqKey(token.getProgramId());
+ String programTokensKey = programTokensKey(token.getProgramId());
String tokenIdStr = token.getId().asString();
-
- // Sorted Set의 score로 사용할 진입 시각 (epoch milli)
long issuedAtEpochMilli = token.getIssuedAt().toEpochMilli();
+ long ttlSeconds = properties.waitingTtl().getSeconds();
+
+ Long result = redisTemplate.execute(
+ enqueueScript,
+ List.of(userProgramKey, programKey, tokenKey, seqKey, programTokensKey),
+ tokenIdStr,
+ token.getUserId().asString(),
+ token.getProgramId().asString(),
+ String.valueOf(issuedAtEpochMilli),
+ token.getStatus().name(),
+ String.valueOf(ttlSeconds)
+ );
- Duration ttl = properties.waitingTtl();
-
- // 1단계: 역인덱스를 락처럼 사용하여 중복 진입 방지
- Boolean acquired = redisTemplate.opsForValue()
- .setIfAbsent(userProgramKey, tokenIdStr, ttl);
-
- if (!Boolean.TRUE.equals(acquired)) {
+ if (result == null || result == 0L) {
throw new DuplicateTokenException();
}
-
- // 2단계: 나머지 키를 트랜잭션으로 저장
- Map tokenFields = Map.of(
- FIELD_USER_ID, token.getUserId().asString(),
- FIELD_PROGRAM_ID, token.getProgramId().asString(),
- FIELD_ISSUED_AT, String.valueOf(issuedAtEpochMilli),
- FIELD_STATUS, token.getStatus().name()
- );
-
- // 트랜잭션으로 Sorted Set + Hash 저장 + Hash TTL 설정
- redisTemplate.execute(new SessionCallback>() {
- @SuppressWarnings({"unchecked"})
- @Override
- public List