Skip to content

Commit ba74e80

Browse files
committed
feat: 이동 윈도 카운터 생성, 조회 기능 구현
1 parent 706a494 commit ba74e80

File tree

5 files changed

+200
-1
lines changed

5 files changed

+200
-1
lines changed

Diff for: build.gradle

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ repositories {
2222
}
2323

2424
dependencies {
25-
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
2625
implementation 'org.springframework.boot:spring-boot-starter-web'
2726
compileOnly 'org.projectlombok:lombok'
2827
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.systemdesign.slidingwindowcounter.controller;
2+
3+
import com.systemdesign.slidingwindowcounter.dto.response.SlidingWindowCounterProfileResponse;
4+
import com.systemdesign.slidingwindowcounter.dto.response.SlidingWindowCounterResponse;
5+
import com.systemdesign.slidingwindowcounter.service.SlidingWindowCounterService;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.*;
9+
import reactor.core.publisher.Flux;
10+
import reactor.core.publisher.Mono;
11+
12+
import static org.springframework.http.HttpStatus.*;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping("sliding-window-counter")
17+
public class SlidingWindowCounterController {
18+
19+
private final SlidingWindowCounterService slidingWindowCounterService;
20+
21+
@GetMapping
22+
public Mono<ResponseEntity<Flux<SlidingWindowCounterResponse>>> findAllSlidingWindowLog() {
23+
return Mono.just(
24+
ResponseEntity.status(OK)
25+
.body(slidingWindowCounterService.findAllSlidingWindowCounter())
26+
);
27+
}
28+
29+
@PostMapping
30+
public Mono<ResponseEntity<SlidingWindowCounterProfileResponse>> createSlidingWindowCounter() {
31+
return slidingWindowCounterService.createSlidingWindowCounter()
32+
.map(response -> ResponseEntity.status(CREATED).body(response));
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.systemdesign.slidingwindowcounter.dto.response;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@Builder
10+
public class SlidingWindowCounterProfileResponse {
11+
12+
private List<SlidingWindowCounterResponse> counters;
13+
14+
public static SlidingWindowCounterProfileResponse from(List<SlidingWindowCounterResponse> counters) {
15+
return SlidingWindowCounterProfileResponse.builder()
16+
.counters(counters)
17+
.build();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.systemdesign.slidingwindowcounter.dto.response;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@Builder
8+
public class SlidingWindowCounterResponse {
9+
10+
private String key;
11+
private Long requestCount;
12+
13+
public static SlidingWindowCounterResponse from(String key, Long requestCount) {
14+
return SlidingWindowCounterResponse.builder()
15+
.key(key)
16+
.requestCount(requestCount)
17+
.build();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.systemdesign.slidingwindowcounter.service;
2+
3+
import com.systemdesign.slidingwindowcounter.dto.response.SlidingWindowCounterProfileResponse;
4+
import com.systemdesign.slidingwindowcounter.dto.response.SlidingWindowCounterResponse;
5+
import com.systemdesign.slidingwindowcounter.exception.RateExceptionCode;
6+
import com.systemdesign.slidingwindowcounter.exception.RateLimitExceededException;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.data.domain.Range;
10+
import org.springframework.data.redis.core.ReactiveRedisTemplate;
11+
import org.springframework.stereotype.Service;
12+
import reactor.core.publisher.Flux;
13+
import reactor.core.publisher.Mono;
14+
15+
import java.util.List;
16+
import java.util.UUID;
17+
18+
@Slf4j
19+
@Service
20+
@RequiredArgsConstructor
21+
public class SlidingWindowCounterService {
22+
23+
private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
24+
private final static String SLIDING_WINDOW_KEY = "SlidingWindowCounter:"; // 키
25+
private final static long SLIDING_WINDOW_COUNTER_MAX_REQUEST = 1000; // 최대 요청 허용 수
26+
private final static long SLIDING_WINDOW_COUNTER_DURATION = 60; // 60초
27+
28+
public Mono<SlidingWindowCounterProfileResponse> createSlidingWindowCounter() {
29+
long currentTimestamp = System.currentTimeMillis();
30+
String redisKey = generateRedisKey("requests");
31+
log.info("Sliding Window Counter created. key: {}", redisKey);
32+
33+
// 현재 윈도우의 시작 시간 계산
34+
double startTimeCurrentWindow = calculateTimeRange(currentTimestamp);
35+
36+
// 이전 윈도우의 시작 시간 계산
37+
double startTimePreviousWindow = calculateTimeRange(currentTimestamp - SLIDING_WINDOW_COUNTER_DURATION * 1000);
38+
39+
//식별자 - 현재의 타임 스탬프와 UUID 조합
40+
String uniqueRequestIdentifier = String.valueOf(currentTimestamp) + ":" + UUID.randomUUID().toString();
41+
42+
return reactiveRedisTemplate.opsForZSet()
43+
.add(redisKey, uniqueRequestIdentifier, currentTimestamp)
44+
.flatMap(success -> {
45+
if (!success) {
46+
return Mono.empty();
47+
}
48+
49+
// 이전 윈도우의 요청 수를 가져옴
50+
Mono<Long> currentCountValue = reactiveRedisTemplate.opsForZSet()
51+
.count(redisKey, Range.closed(startTimeCurrentWindow, (double) currentTimestamp))
52+
.defaultIfEmpty(0L);
53+
54+
return currentCountValue.flatMap(previousCount -> {
55+
double overlapRate = calculateOverlapRate(currentTimestamp);
56+
57+
// 현재 요청을 포함한 최종 요청 수 계산
58+
long totalCount = Math.round(1 + (previousCount * overlapRate));
59+
log.info("Adjusted Sliding Window Counter count: {}", totalCount);
60+
61+
log.info("Sliding Window Counter count: {}", totalCount);
62+
log.debug("previousCountValue Sliding Window Counter count: {}", currentCountValue);
63+
log.debug("overlapRate Window Counter count: {}", overlapRate);
64+
if (totalCount >= SLIDING_WINDOW_COUNTER_MAX_REQUEST) {
65+
log.error("Rate limit exceeded. key: {}", redisKey);
66+
return Mono.<SlidingWindowCounterProfileResponse>error(
67+
new RateLimitExceededException(RateExceptionCode.COMMON_TOO_MANY_REQUESTS, totalCount)
68+
);
69+
} else {
70+
return Mono.just(
71+
SlidingWindowCounterProfileResponse.from(
72+
List.of(SlidingWindowCounterResponse.from(redisKey, totalCount))
73+
)
74+
);
75+
}
76+
});
77+
})
78+
// 에러 로깅
79+
.doOnError(error -> {
80+
log.error("An error occurred: {}", error.getMessage());
81+
})
82+
.onErrorResume(error -> {
83+
// 에러가 발생했을 때 에러 처리
84+
return Mono.error(new RateLimitExceededException(RateExceptionCode.COMMON_TOO_MANY_REQUESTS, 0L));
85+
});
86+
}
87+
88+
public Flux<SlidingWindowCounterResponse> findAllSlidingWindowCounter() {
89+
String redisKey = generateRedisKey("requests");
90+
long currentTimestamp = System.currentTimeMillis();
91+
log.info("Sliding Window Counter find all. key: {}", redisKey);
92+
93+
return reactiveRedisTemplate.opsForZSet().rangeByScore(redisKey,
94+
Range.closed(Double.MIN_VALUE, (double) currentTimestamp))
95+
//Range.closed(calculateTimeRange(currentTimestamp), (double) currentTimestamp))
96+
.map(value -> {
97+
String[] parts = value.toString().split(":");
98+
long timestamp = Long.parseLong(parts[0]);
99+
log.info("Sliding Window Counter value: {}", timestamp);
100+
return SlidingWindowCounterResponse.from(redisKey, timestamp);
101+
});
102+
}
103+
104+
//이전 윈도와 현재 윈도에 겹치는 시간 비율 계산
105+
private double calculateOverlapRate(long currentTimestamp) {
106+
//이전 윈도우의 시작 시간
107+
double startTimePreviousWindow = calculateTimeRange(currentTimestamp - SLIDING_WINDOW_COUNTER_DURATION * 1000);
108+
109+
// 현재 윈도우의 시작 시간
110+
double startTimeCurrentWindow = calculateTimeRange(currentTimestamp);
111+
112+
// 겹치는 시간의 길이를 계산
113+
double overlapDuration = (startTimeCurrentWindow - startTimePreviousWindow) / 1000.0;
114+
115+
// 겹치는 시간 비율을 계산 (윈도우 지속 시간으로 나눔)
116+
double overlapRate = overlapDuration / SLIDING_WINDOW_COUNTER_DURATION;
117+
118+
return overlapRate;
119+
}
120+
121+
private double calculateTimeRange(long currentTimestamp) {
122+
return currentTimestamp - SLIDING_WINDOW_COUNTER_DURATION * 1000;
123+
}
124+
125+
private String generateRedisKey(String requestType) {
126+
return SLIDING_WINDOW_KEY + requestType;
127+
}
128+
}

0 commit comments

Comments
 (0)