Skip to content

Commit bcc3e34

Browse files
committed
feat(Entry): Implement auto-stock restoration on reservation token expiration
1 parent a9f39b1 commit bcc3e34

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

entry-service/src/main/java/com/axon/entry_service/config/RedisConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.data.redis.connection.RedisConnectionFactory;
66
import org.springframework.data.redis.core.RedisTemplate;
7+
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
78
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
89
import org.springframework.data.redis.serializer.StringRedisSerializer;
910

@@ -30,4 +31,11 @@ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisC
3031
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
3132
return redisTemplate;
3233
}
34+
35+
@Bean
36+
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
37+
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
38+
container.setConnectionFactory(connectionFactory);
39+
return container;
40+
}
3341
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.axon.entry_service.service.redisListener;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.data.redis.connection.Message;
5+
import org.springframework.data.redis.core.StringRedisTemplate;
6+
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
7+
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.nio.charset.StandardCharsets;
11+
import java.time.Duration;
12+
import java.util.Base64;
13+
14+
@Slf4j
15+
@Component
16+
public class ReservationExpirationListener extends KeyExpirationEventMessageListener {
17+
private final StringRedisTemplate redisTemplate;
18+
19+
public ReservationExpirationListener(RedisMessageListenerContainer listener, StringRedisTemplate redisTemplate) {
20+
super(listener);
21+
this.redisTemplate = redisTemplate;
22+
}
23+
24+
@Override
25+
public void onMessage(Message message, byte[] pattern) {
26+
String expiredKey = message.toString(); // 1차 토큰 전체 문장
27+
28+
// 1차 토큰 만료인지 확인
29+
if(!expiredKey.startsWith("RESERVATION_TOKEN:")) {return;}
30+
31+
try {
32+
String tokenValue = expiredKey.replace("RESERVATION_TOKEN:", "");
33+
String decoded = new String(Base64.getDecoder().decode(tokenValue), StandardCharsets.UTF_8);
34+
35+
String[] parts = decoded.split(":");
36+
if(parts.length < 2) {
37+
log.warn("Invalid token format ignored: {}", decoded);
38+
return;
39+
}
40+
41+
String userId = parts[0];
42+
String campaignActivityId = parts[1];
43+
44+
// 각 파드간 중복 실행 방지용
45+
String lockKey = "processed:restore:" + tokenValue;
46+
Boolean isWinner = redisTemplate.opsForValue()
47+
.setIfAbsent(lockKey, "1", Duration.ofMinutes(1));
48+
49+
if(Boolean.TRUE.equals(isWinner)) {
50+
String counterKey = "campaign:" + campaignActivityId + ":counter";
51+
Long remaining = redisTemplate.opsForValue().decrement(counterKey);
52+
53+
String usersKey = "campaign:" + campaignActivityId + ":users";
54+
redisTemplate.opsForSet().remove(usersKey, userId);
55+
56+
log.info("♻️ Stock Restored! Activity: {}, User: {}, Remaining: {}", campaignActivityId, userId, remaining);
57+
} else {
58+
log.debug("Another pod Accessed and Skipped decrease request");
59+
}
60+
} catch (Exception e) {
61+
log.error("1차 토큰 만료로 인한 재고 복구 실패 token={}", expiredKey, e);
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)