2024.07.13 - [Spring/대용량 트래픽] - Blockhound로 디버깅하기
Spring Webflux 프레임워크와, Redis를 결합하면 트래픽이 과도하게 쏠리는 경우 매우 빠른속도로 이를 해결할수있다.
위 이미지에서 queue가 요청처리과정을 조절하고, 전달해주는 대기열 시스템이다.
위와같은 아키텍처가 없으면 짧은시간에 트래픽이 몰릴경우, 어떤 요청은 실패를 하고 시스템은 엉망이 될수있다.
아래와 같은경우가 이런 아키텍처를 활용할 경우이다.
- 티켓팅
- 선착순 이벤트
- 수강신청
트래픽 유형
- 트래픽 선형증가 -> WAS추가, DB스펙변경
- 트래픽 특정 패턴변화(ex. 출퇴근시간 네비게이션, 배달음식) -> 서버리소스 자동 scale out
- 특정순간 대량의 트래픽(spike sum) - 읽기위주? 쓰기위주?
- 예측가능 - 티켓팅, 수강신청
- 예측불가능 - 재난상황
MY PROJECT
- 예측 가능한 시기
- 짧은 시간동안 대량의 트래픽 유입되는 상황
- 특정 웹페이지에 대해 사용자 진입 수 제한
- 대기 사용자에 대해 순차적 진입
위와 같은 조건을 만족하는 서비스를 만들어보겠다.
준비할 아키텍처는 다음과 같다.
- Spring MVC기반 타겟 웹페이지 준비
- 접속자 대기열 준비
- Spring Webflux
- Redis(Queue : Sorted Set으로 접속자 관리) Key : 사용자ID, Value : 요청시간 -> 선착순 입장 보장
- 스케줄러를 통해 일정주기마다 대기열에 있는 사용자를 접속가능하게 해준다.
- 스케줄러를 통해 접속가능해진 사용자는 원래 이동해야할 웹페이지로 이동한다.
서비스 흐름도
- 사용자 -> MVC 요청
- MVC -> Webflux 요청 : 웹페이지 진입 가능 확인(대기열 시스템 API서버로 질의)
- 진입이 허용되지 않는 경우 : 대기열 등록 및 대기 웹페이지 응답
- 웹 페이지 진입 가능 확인 및 Redirect(주기적으로)
- 진입이 허용된 경우 : 실제 웹페이지 재진입(MVC를 거쳐서 응답)
개발환경준비(MVC)
application.yml
server:
port: 9000
templates/index.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<title>환영합니다</title>
<meta charset="utf-8">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.main-content {
text-align: center;
}
.logo-image {
width: 200px;
height: 200px;
border-radius: 50%;
border: 2px solid #007bff;
margin-bottom: 20px;
}
.description {
font-size: 18px;
margin-bottom: 20px;
}
.cta-button {
background-color: #007bff;
color: #fff;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.cta-button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="main-content">
<h1>환영합니다</h1>
<p class="description">Redis는 key/value 기반의 In-memory 데이터 저장소로서, 빠른 읽기와 쓰기 속도를 제공하며 다양한 데이터 구조를 지원하여 복잡한 데이터 조작도 간단하게 처리할 수 있습니다.</p>
<p>Spring WebFlux는 Reactive Streams을 기반으로 하여 요청에 대한 응답을 비동기적으로 처리하며, 논블로킹 I/O 모델을 활용하여 높은 성능과 확장성을 제공합니다.</p>
<button class="cta-button">자세히 알아보기</button>
</div>
</body>
</html>
Controller 생성
@SpringBootApplication
@Controller
public class MvcwebsiteApplication {
public static void main(String[] args) {
SpringApplication.run(MvcwebsiteApplication.class, args);
}
@GetMapping("/")
public String index() {
return "index";
}
}
MVC 접속 UI
개발환경준비(Webflux)
** Mac 유저들은 밑에 의존성을 추가해줘야한다.
implementation 'io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64'
application.yml
server:
port: 9010
Redis 실행
- --rm : 컨테이너가 종료되면 자동으로 컨테이너를 삭제
docker run -it --rm -p 6379:6379 redis:6.2
MVC -> Webflux -> Redis로의 flow에서 MVC에서 대기인원이 가득차있다고 웹사이트를 띄워주고, Webflux에서는 대기열에 등록시키는 API(들어온 순서대로)
Webflux 프로젝트 초기세팅
application.yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
WebfluxflowApplication.java
@SpringBootApplication
@RequiredArgsConstructor
public class WebfluxflowApplication implements ApplicationListener<ApplicationReadyEvent> {
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
public static void main(String[] args) {
SpringApplication.run(WebfluxflowApplication.class, args);
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
reactiveRedisTemplate.opsForValue().set("testKey", "testValue").subscribe();
}
}
이제 먼저 들어온사람이 높은 우선순위를 가질수있도록 할것이다. -> Redis의 Sorted Set 자료구조를 통해서
- Key : 사용자 아이디
- Value : 들어온 시간
UserQueueController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/queue")
public class UserQueueController {
private final UserQueueService userQueueService;
// 등록 할 수 있는 API path
@PostMapping("")
public Mono<?> registerUser(@RequestParam(name = "user_id") Long userId){
return userQueueService.registerWaitQueue(userId);
}
}
UserQueueService.java
@Service
@RequiredArgsConstructor
public class UserQueueService {
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
// 대기열 등록 API
public Mono<Boolean> registerWaitQueue(final Long userId){
// redis sortedSet
// key : userId
// value : unix timestamp
long unixTimestamp = Instant.now().getEpochSecond();
return reactiveRedisTemplate.opsForZSet().add("user-queue", userId.toString(), unixTimestamp);
}
}
- add함수의 리턴형이 Boolean이므로 함수 리턴형도 맞춰준다.
잘 작동한다.
- 사실 위 API가지고는 등록되었는지 true/false만 확인할수 있고, 몇번째로 대기하고 있는지는 확인할수 없다.
-> Zset의 기능인 rank() 메서드 (Long을 리턴)를 활용해서 확인할수 있다.
-> 숫자를 1씩 더해서 랭크를 1부터 직관화시키자.
UserQueueController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/queue")
public class UserQueueController {
private final UserQueueService userQueueService;
// 등록 할 수 있는 API path
@PostMapping("")
public Mono<RegisterUserResponse> registerUser(@RequestParam(name = "user_id") Long userId){
return userQueueService.registerWaitQueue(userId)
.map(RegisterUserResponse::new);
}
}
UserQueueService.java
@Service
@RequiredArgsConstructor
public class UserQueueService {
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
// 대기열 등록 API
public Mono<Long> registerWaitQueue(final Long userId){
// redis sortedSet
// key : userId
// value : unix timestamp
long unixTimestamp = Instant.now().getEpochSecond();
return reactiveRedisTemplate.opsForZSet().add("user-queue", userId.toString(), unixTimestamp)
.filter(i -> i)
.switchIfEmpty(Mono.error(new Exception("already register user . . . .")))
.flatMap(i -> reactiveRedisTemplate.opsForZSet().rank("user-queue", userId.toString()))
.map(i -> i >= 0 ? i+1 : i);
}
}
- filter(i -> i) : true/false가 인자로 넘어오는데, 이것을 받아서 true일때만 메서드가 true를 넘겨주고, false이면 아무것도 넘겨주지 않으므로 이미 대기열에 등록된 사용자에 대해서는 500에러를 발생시킬수있다.
- rank()메서드로 요청순서를 응답으로 받을수 있다.
RegisterUserResponse.java
public record RegisterUserResponse(Long rank) {
}
- 응답으로 Record로 감싸서 rank를 리턴할것이다.
- 단순히 말하자면 Entity를 쓰는것보다 훨씬 간단하게 저장할수 있는 DTO이다.
Record가 뭔지 자세히 알아보다가 잘 정리되어 있는 글을 보게되어 첨부한다.
https://cjh970422.tistory.com/entry/SpringBoot%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-Record
이제 대기열을 다수로 운영할수 있게 컨트롤러, 서비스 인자에 String queue를 추가해준다. (인자로 아무것도 주지않으면 default)
에러 반환 수정
사실 이미 대기열에 등록된 사용자를 다시 등록한다고해서 서버측 에러라고 할순없다. 500에러가 아닌, 우리가 원하는 형식의 에러를 클라이언트에게 알려주기 위해 에러 검증 로직을 추가하자.
ApplicationException.java
@AllArgsConstructor
@Getter
public class ApplicationException extends RuntimeException {
private HttpStatus status;
private String code;
private String reason;
}
- 에러 코드 객체 생성을 손쉽게 만들어주는 집합이다.
ErrorCode.java
@AllArgsConstructor
public enum ErrorCode {
QUEUE_ALREADY_REGISTERED_USER(HttpStatus.CONFLICT, "UQ-001", "Already registered in queue");
private final HttpStatus httpStatus;
private final String code;
private final String reason;
public ApplicationException build(){
return new ApplicationException(httpStatus, code, reason);
}
public ApplicationException build(Object ...args){
return new ApplicationException(httpStatus, code, reason.formatted(args));
}
}
-> 이제 서비스 로직에서 단순히 예외를 발생시켰던 부분을
.switchIfEmpty(Mono.error(new Exception("already register user . . . .")))
.switchIfEmpty(Mono.error(ErrorCode.QUEUE_ALREADY_REGISTERED_USER.build()))
이렇게 원하는대로 설정할수 있다. 그리고 이 예외에 대해 원하는 메세지를 리턴하기 위한부분을 구현해줘야한다.
ApplicationAdvice.java
@RestControllerAdvice
public class ApplicationAdvice {
@ExceptionHandler(ApplicationException.class)
public Mono<ResponseEntity<ServerExceptionResponse>> handleApplicationException(ApplicationException ex) {
return Mono.just(ResponseEntity
.status(ex.getStatus())
.body(new ServerExceptionResponse(ex.getCode(), ex.getReason())));
}
public record ServerExceptionResponse(String code, String reason){
}
}
MVC에서 예외 검증하는 부분과 같이 진행한다.
- @RestControllerAdvice : 전역적으로 예외를 처리할수 있도록 클래스를 설정하고, 응답을 Json형식으로 내려준다.
- @ExceptionHandler : 어느 클래스에서 생성된 예외에 대해 처리할지 클래스를 지정한다.
따라서 ResponseEntity에 응답을 감싸서 주었고, ResponseEntity body에 대한 부분은 따로 레코드 만들어서, 인자로 넘겨주었다.
테스트
대기열 진입여부 API, 진입허용 API
- 대기열에 있다가 사용자가 웹페이지에 들어올수 있는 순서가 된다면 대기열에서 삭제시키고, 진입이 허용된 사용자들로 구성된 queue에 등록시켜야한다. -> 앞서 사용한 Sorted Set 자료구조를 하나 더 만들어야한다.
진입허용 API
UserQueueService.java
// 진입을 허용
public Mono<Long> allowUser(final String queue, final Long count){
// 진입을 허용하는 단계
// 1. wait queue 사용자를 제거
// 2. proceed queue 사용자를 추가
return reactiveRedisTemplate.opsForZSet().popMin(USER_QUEUE_WAIT_KEY.formatted(queue), count)
.flatMap(member -> reactiveRedisTemplate.opsForZSet().add(USER_QUEUE_PROCEED_KEY.formatted(queue), member.getValue(), Instant.now().getEpochSecond()))
.count();
}
UserQueueController.java
@PostMapping("/allow")
public Mono<?> allowUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "count") Long count) {
return userQueueService.allowUser(queue, count);
}
- wait queue에 3명이 있고, count로 5를 주었다면 Service단에서 집계함수 count에 의해 3이 리턴될것이다. 이에 맞게 응답 양식을 구현하자.
AllowUserResponse.java
public record AllowUserResponse(Long requestCount, Long allowedCount) {
}
컨트롤러는 다음과 같이 수정된다.
@PostMapping("/allow")
public Mono<AllowUserResponse> allowUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "count") Long count) {
return userQueueService.allowUser(queue, count)
.map(allowed -> new AllowUserResponse(count, allowed));
}
맥 유저는 터미널에서 요청을 보내 바로 응답을 확인할수있다.
❯ curl -X POST localhost:9010/api/v1/queue/allow\?count=3
대기열 진입여부 API
UserQueueService.java
// 진입이 가능한 상태?
public Mono<Boolean> isAllowed(final String queue, final Long userId){
return reactiveRedisTemplate.opsForZSet().rank(USER_QUEUE_PROCEED_KEY.formatted(queue), userId.toString())
.defaultIfEmpty( -1L)
.map(rank -> rank >= 0);
}
UserQueueController.java
@GetMapping("/allowed")
public Mono<AllowedUserResponse> isAllowedUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId){
return userQueueService.isAllowed(queue, userId)
.map(AllowedUserResponse::new);
}
AllowedUserResponse.java
public record AllowedUserResponse(Boolean allowed) {
}
Service 단 테스트
- Redis를 사용해서 코드에 대한 결과를 받고 검증했다. 이를 테스트 코드로 짤려고하면 어떻게 해야할까? -> embedded redis - 격리된 테스트 전용 redis사용!
build.gradle
testImplementation 'com.github.codemonstur:embedded-redis:1.0.0'
embedded-redis를 사용하기 위해 의존성을 추가해준다.
테스트 설정파일
@TestConfiguration
public class EmbeddedRedis {
private final RedisServer redisServer;
public EmbeddedRedis() throws IOException {
this.redisServer = new RedisServer(63790);
}
@PostConstruct
public void start() throws IOException {
this.redisServer.start();
}
@PreDestroy
public void stop() throws IOException {
this.redisServer.stop();
}
}
- @PostConstruct : bean이 생성되고 딱 한번만 실행
- @PreDestroy : 어플리케이션 컨텍스트에서 bean을 제거하기 직전 딱 한번만 실행
- 63790 : 이미 실행하고 있는 도커 컨테이너의 레디스 포트번호인 6379번과 겹치지 않기 위해 따로 사용
-> 이를 위해 .yml파일에도 설정사항을 추가해줘야한다.
application.yml
spring:
config:
activate:
on-profile: test
data:
redis:
host: 127.0.0.1
port: 63790
UserQueueServiceTest.java
@SpringBootTest
@Import({EmbeddedRedis.class})
@ActiveProfiles("test")
class UserQueueServiceTest {
@Autowired
private UserQueueService userQueueService;
@Autowired
private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@BeforeEach
public void beforeEach() {
ReactiveRedisConnection reactiveConnection = reactiveRedisTemplate.getConnectionFactory().getReactiveConnection();
reactiveConnection.serverCommands().flushAll().subscribe();
}
@Test
void registerWaitQueue() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L))
.expectNext(1L)
.verifyComplete();
StepVerifier.create(userQueueService.registerWaitQueue("default", 101L))
.expectNext(2L)
.verifyComplete();
StepVerifier.create(userQueueService.registerWaitQueue("default", 102L))
.expectNext(3L)
.verifyComplete();
}
@Test
void alreadyRegisterWaitQueue() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L))
.expectNext(1L)
.verifyComplete();
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L))
.expectError(ApplicationException.class)
.verify();
}
@Test
void emptyAllowUser() {
StepVerifier.create(userQueueService.allowUser("default", 3L))
.expectNext(0L)
.verifyComplete();
}
@Test
void allowUser() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
.then(userQueueService.registerWaitQueue("default", 101L))
.then(userQueueService.registerWaitQueue("default", 102L))
.then(userQueueService.allowUser("default", 2L)))
.expectNext(2L)
.verifyComplete();
}
@Test
void allowUser2() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
.then(userQueueService.registerWaitQueue("default", 101L))
.then(userQueueService.registerWaitQueue("default", 102L))
.then(userQueueService.allowUser("default", 5L)))
.expectNext(3L)
.verifyComplete();
}
@Test
void allowUserAfterRegisterWaitQueue() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
.then(userQueueService.registerWaitQueue("default", 101L))
.then(userQueueService.registerWaitQueue("default", 102L))
.then(userQueueService.allowUser("default", 3L))
.then(userQueueService.registerWaitQueue("default", 200L)))
.expectNext(1L)
.verifyComplete();
}
@Test
void isNotAllowed() {
StepVerifier.create(userQueueService.isAllowed("default", 100L))
.expectNext(false)
.verifyComplete();
}
@Test
void isNotAllowed2() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
.then(userQueueService.allowUser("default", 3L))
.then(userQueueService.isAllowed("default", 101L)))
.expectNext(false)
.verifyComplete();
}
@Test
void isAllowed() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
.then(userQueueService.allowUser("default", 3L))
.then(userQueueService.isAllowed("default", 100L)))
.expectNext(true)
.verifyComplete();
}
}
- @ActiveProfiles("test") : .yml 파일에서 설정한 정보를 바탕으로 port번호 63790에 접속한다는 의미이다.
- @BeforeEach : 각 테스트 메서드마다 서로 독립적으로 실행될수 있게 모든 테스트 정보를 테스트 이전에 초기화 시킨후 진행시킬수 있는 기능이다.
접속 대기 웹페이지
- 템플릿 엔진 기반으로 사용자의 웹페이지 접속 대기 순번을 알려주는 웹페이지를 구현한다.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>접속자대기열시스템</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.message {
text-align: center;
padding: 20px;
font-size: 18px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="message">
<h1>접속량이 많습니다.</h1>
<span>현재 대기 순번 </span><span id="number">[[${number}]]</span><span> 입니다.</span>
<br/>
<p>서버의 접속량이 많아 시간이 걸릴 수 있습니다.</p>
<p>잠시만 기다려주세요.</p>
<p id="updated"></p>
<br/>
</div>
<script>
function fetchWaitingRank() {
const queue = '[[${queue}]]';
const userId = '[[${userId}]]';
const queryParam = new URLSearchParams({queue: queue, user_id: userId});
fetch('/api/v1/queue/rank?' + queryParam)
.then(response => response.json())
.then(data => {
if(data.rank < 0) {
document.querySelector('#number').innerHTML = 0;
document.querySelector('#updated').innerHTML = new Date();
const newUrl = window.location.origin + window.location.pathname + window.location.search;
window.location.href = newUrl;
return;
}
document.querySelector('#number').innerHTML = data.rank;
document.querySelector('#updated').innerHTML = new Date();
})
.catch(error => console.error(error));
}
setInterval(fetchWaitingRank, 3000);
</script>
</body>
</html>
WaitingRoomController.java
@Controller
public class WaitingRoomController {
@GetMapping("/waiting-room")
Mono<Rendering> waitingRoomPage(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId){
return Mono.just(Rendering.view("waiting-room.html").build());
}
}
- 이제 필요한것은 사용자의 대기번호이다.(주기적으로 체크가능한)
UserQueueService.java
public Mono<Long> getRank(final String queue, final Long userId){
return reactiveRedisTemplate.opsForZSet().rank(USER_QUEUE_WAIT_KEY.formatted(queue), userId.toString())
.defaultIfEmpty(-1L)
.map(rank -> rank >= 0 ? rank + 1 : rank);
}
UserQueueServiceTest.java
@Test
void getRank() {
StepVerifier.create(userQueueService.registerWaitQueue("default", 100L)
.then(userQueueService.getRank("default", 100L)))
.expectNext(1L)
.verifyComplete();
StepVerifier.create(userQueueService.registerWaitQueue("default", 101L)
.then(userQueueService.getRank("default", 101L)))
.expectNext(2L)
.verifyComplete();
}
@Test
void emptyRank(){
StepVerifier.create(userQueueService.getRank("default", 100L))
.expectNext(-1L)
.verifyComplete();
}
UserQueueController.java
@GetMapping("/rank")
public Mono<RankNumberResponse> getRankUser(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId){
return userQueueService.getRank(queue, userId)
.map(RankNumberResponse::new);
}
RankNumberResponse.java
public record RankNumberResponse(Long rank) {
}
WaitingRoomController 완성 (UserQueueService 이용)
@Controller
@RequiredArgsConstructor
public class WaitingRoomController {
private final UserQueueService userQueueService;
@GetMapping("/waiting-room")
Mono<Rendering> waitingRoomPage(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId){
// 대기 등록
// 웹페이지 필요한 데이터를 전달
return userQueueService.registerWaitQueue(queue, userId)
.onErrorResume(ex -> userQueueService.getRank(queue, userId))
.map(rank -> Rendering.view("waiting-room.html")
.modelAttribute("number", rank)
.modelAttribute("userId", userId)
.modelAttribute("queue", queue)
.build());
}
}
이제 대기순번까지 정확하게 나온다.
어떤 View로 렌더링할지 로직 구현
- 입장이 허용되어 page redirect(이동)이 가능한 상태?
- 어디로 이동해야 하는가?
로직이 추가된 WaitingRoomController.java
@Controller
@RequiredArgsConstructor
public class WaitingRoomController {
private final UserQueueService userQueueService;
@GetMapping("/waiting-room")
Mono<Rendering> waitingRoomPage(@RequestParam(name = "queue", defaultValue = "default") String queue,
@RequestParam(name = "user_id") Long userId,
@RequestParam(name = "redirect_url") String redirectUrl) {
return userQueueService.isAllowed(queue, userId)
.filter(allowed -> allowed)
.flatMap(allowed -> Mono.just(Rendering.redirectTo(redirectUrl).build()))
.switchIfEmpty(userQueueService.registerWaitQueue(queue, userId)
.onErrorResume(ex -> userQueueService.getRank(queue, userId))
.map(rank -> Rendering.view("waiting-room.html")
.modelAttribute("number", rank)
.modelAttribute("userId", userId)
.modelAttribute("queue", queue)
.build()));
}
}
진입가능한지 여부를 판단하기 위해 리다이렉트할 url이 파라미터로 필요하다.
http://localhost:9010/waiting-room?user_id=18&redirect_url=https://www.naver.com
나는 다음과 같이 입력해봤다.
아직 waiting queue에 있는 사용자이므로 다음과 같이 화면이 렌더링된다. proceed queue에 진입이 허가되지 않은 사용자는 자동으로 waiting queue에 추가되고 위와 같은 화면이 렌더링 된다.
대기 순번이 1번인 123번 아이디 사용자 접속허용 시켜보자.
curl -X POST localhost:9010/api/v1/queue/allow\?count=1
- 다음 단계는 수동으로 URL을 입력하는것이 아닌, 자동으로 내가 만들어놓은 MVC 웹페이지로 이동할수 있게 구현해야한다.
2024.07.23 - [Spring/대용량 트래픽] - Spring Webflux와 Redis이용해서 접속자 대기열 시스템 만들기(2)
'Spring > 대용량 트래픽' 카테고리의 다른 글
Spring Webflux와 Redis이용해서 접속자 대기열 시스템 만들기(2) (1) | 2024.07.23 |
---|---|
Blockhound로 디버깅하기 (0) | 2024.07.13 |
Spring MVC vs Webflux 성능 비교(with Jmeter) (0) | 2024.07.13 |
Reactive Redis 사용법 (1) | 2024.07.12 |
Reactive Redis란? (0) | 2024.07.07 |