Redis를 이용한 대기열 구현
Redis를 이용한 대기열 구현
목적
기존의 데이터베이스로 관리하던 대기열 시스템을 Redis를 이용하여 데이터베이스의 부하를 줄인다.
대기열 개발방식 고찰
은행창구 방식
- 만료된 대기열의 큐만큼 서비스를 이용할 수 있는 토큰을 활성화
- 현재 대기열은 데이터베이스를 이용하여 은행 창구 방식으로 관리된다.
- 대기열 시스템을 요약하자면 아래와 같다.
- 콘서트 스케줄 날짜 조회를 시도하는 사용자에게 토큰을 발급하고 데이터베이스에 저장한다. 토큰의 상태값은
WAIT이다. - 만료된 토큰을 기준으로 스케줄러를 이용하여 일정시간마다
WAIT의 토큰을ACTIVE로 변경한다. (만료시간은 5분 - 평균 유저활동 시간) - 5분 내에 콘서트 예약이 이루어지지 않는다면 스케줄러를 이용하여
ACTIVE에서EXPIRE로 변경한다. - 예약이 완료된다면
ACTIVE에서EXPIRE로 변경한다.
- 콘서트 스케줄 날짜 조회를 시도하는 사용자에게 토큰을 발급하고 데이터베이스에 저장한다. 토큰의 상태값은
- 장점
- 서비스를 이용하는 유저를 일정한 수 만큼 유지할 수 있다.
- 단점
- 유저의 행동에 따라 대기열의 전환시간이 불규칙하다.
놀이공원 방식
- N초마다 M개의 Active token으로 전환한다.
- Redis를 이용하여 개선될 방식이다.
- 장점
- 대기열의 사용자들이 일정한 시간으로 서비스의 진입이 가능하다.
- 단점
- 서비스를 이용하는 사용자의 수가 보장되지 않는다.
대기열 기능개발
- RDB에서 관리하던 기존 대기열을 아래의 Redis 자료구조를 이용하여 처리함
- 기존
- 데이터베이스를 이용한 대기열 관리
- 신규
- 2가지 Redis 자료구조를 이용하여 대기열을 구성함
- 대기큐(WaitingQueue) : Sorted Set 자료구조를 이용
- 활성큐(ActiveQueue) : Set 자료구조 이용
- 기존
- 큐의 만료시간 설정
- 기존 : 스케줄러를 이용하여 5분경과된 데이터의 상태값 변경
- 신규 : 활성큐의 Key에 TTL을 적용하여 만료시간을 조절
- 만료시간 연장
- 기존 : 토큰을 이용하여 해당 큐를 조회하여 만료시간을 연장함
- 신규 : 토큰을 이용하여 해당 큐의 TTL을 갱신
코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@Service
@RequiredArgsConstructor
public class QueueRedisService {
private final UserAccountRepository userAccountRepository;
private final WaitingQueueRedisRepository waitingQueueRedisRepository;
private final ActiveQueueRedisRepository activeQueueRedisRepository;
/**
대기큐 생성
*/
public String createWaitQueue(Long userId) {
String lockKey = "lock:user:" + userId;
// 10초동안 같은 유저가 큐에 진입하지 못하도록 함
Boolean isUserQueued = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "LOCK:%d".formatted(userId), 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(isUserQueued)) {
String s = redisTemplate.opsForValue().get(lockKey);
log.error("user already in the wait Queue : userId - {}, lockValue - {}", userId, s);
return "";
}
try {
UserAccount userAccount = userAccountRepository.findById(userId);
String token = generateToken();
// 생성된 토큰을 사용자 정보에 저장
userAccount.saveToken(token);
userAccountRepository.save(userAccount);
// 대기열에 사용자의 정보와 토큰을 저장
waitingQueueRedisRepository.save(userAccount.getId(), token);
return token;
} finally {
redisTemplate.delete(lockKey);
}
}
/**
활성큐('Active Queue')를 생성한다.
- 놀이공원 방식의 `대기열`을 구성
- 파라미터(limit)만큼의 큐를 `대기큐`에서 `활성큐`로 변경
- 스케줄러를 사용하여 변경함
*/
public void createActiveQueue(int limit) {
Set<WaitingQueueRedisDto> waitingQueues = waitingQueueRedisRepository.getQueueByRange(limit);
waitingQueues.forEach(dto -> {
activeQueueRedisRepository.save(dto.getToken(), dto.getUserId());
waitingQueueRedisRepository.remove(dto.getToken(), dto.getUserId());
});
}
/**
활성큐의 만료시간을 연장한다.
*/
public void extendExpiration(String token) {
activeQueueRedisRepository.extendExpiration(token);
}
/**
대기큐의 순위를 리턴한다. - 사용자의 접속 순서를 확인할 수 있음
*/
public Long getRank(String token, Long userId) {
return waitingQueueRedisRepository.getRank(token, userId);
}
/**
활성큐를 검증한다.
*/
public boolean verify(String token) {
return activeQueueRedisRepository.verify(token);
}
/**
파라미터 범위만큼 대기큐를 조회한다.
*/
public Set<WaitingQueueRedisDto> getWaitingQueues(int limit) {
return waitingQueueRedisRepository.getQueueByRange(limit);
}
private String generateToken() {
String uuid = UUID.randomUUID().toString();
return uuid.substring(uuid.lastIndexOf("-") + 1);
}
}
부하테스트
목적
부하테스트를 실시하여 대기열에서 처리열로 이동할 수 있는 토큰의 수량 및 시간을 특정할 수 있다.
테스트 환경
- 서버: 로컬 테스트
- 사양: Mac CPU(M3) , RAM(18G)
- 데이터베이스 : MySql (Server version:
8.3.0MySQL Community Server - GPL) - Redis: redis_version:
6.2.14 - 부하테스트 :
k6
테스트 시나리오 및 결과
- 30초 동안 점진적으로 사용자를 1명에서 1000명의 늘린다.
- 30초 동안 1000명의 사용자를 유지한다.
- 30초 동안 점진적으로 사용자를 1000명에서 1명으로 줄인다.
테스트 결과
- 평균: 7.99ms
- 최대: 86.06ms
- 평균 TPS: 660/s
1분간 유저가 호출하는 API
- 2(콘서트 좌석을 조회하는 API, 예약 API) * 1.5 ( 동시성 이슈에 의해 예약에 실패하는 케이스를 위한 재시도 계수(예측치)) = 3
분당 처리할 수 있는 트랜잭션 수: 660/s * 60s = 39600
분당 처리할 수 있는 동시 접속자 수: 39600 / 3(API 수) = 13200 명
데스트 서버를 이용시 1분당 대략적으로 13200명을 동시 처리가능하므로 10초마다 2200명의 토큰을 대기큐에서 활성큐로 전환하여 수용할 수 있다는 추론할 수 있다.
This post is licensed under CC BY 4.0 by the author.
