배경
이전 글에서 Redis 의 Lua Script를 활용해 좌석 선택 기능을 Atomic하게 만들었다. 만들고 나기 의문이 들었다. 내가 만든 기능이 실제로 Atomic한가? 따라서 Atomic한지 테스트해보려고한다.
내가 테스트 하려고 하는 방법은 다음과 같다.
‘읽기 - 쓰기’의 작업 전/후에 로그를 넣어서 같은 좌석을 동시에 다수의 유저가 요청할때 한명만 좌석을 선점하며 동시에 로그가 차례대로 찍히는지 확인할 것이다.
테스트 및 결과
그 전에 Atomic 하게 구현되어 있지 않았을 때 코드를 먼저 테스트해보자.
public void selectSeats(SeatDto seatDto, int userId) {
String redisKey = String.format("%d_%d_%d", seatDto.getScheduleId(),
seatDto.getSeatGradeId(), seatDto.getSeatNumber());
Boolean selected = redisTemplate.hasKey(redisKey);
if (Boolean.TRUE.equals(selected)) {
throw new CustomException(ErrorCode.SEAT_IN_PROGRESS);
} else {
redisTemplate.opsForValue()
.set(redisKey, String.valueOf(userId));
redisTemplate.expire(redisKey, 8, TimeUnit.MINUTES);
}
}
다음과 같이 ‘서로 다른 유저가 같은 좌석을 구매’한다고 가정하고 테스트 코드를 만들었다.
@Getter
@RequiredArgsConstructor
public class SeatDto {
private final Integer scheduleId;
private final Integer seatGradeId;
private final Integer seatNumber;
}
public void test() {
int numRequests = 10;
// 쓰레드 풀 생성
ExecutorService executorService = Executors.newFixedThreadPool(numRequests);
// 여러 사용자가 같은 좌석을 선택하도록 요청을 보냄
for (int i = 0; i < numRequests; i++) {
final int userId = i + 1;
final SeatDto seatDto = new SeatDto(1, 1, 1);
executorService.submit(() -> {
try {
// 좌석 선택
selectSeats(seatDto, userId);
} catch (CustomException e) {
System.out.println(
"User " + userId + " encountered an error: " + e.getMessage());
}
});
}
// 스레드 풀 종료
executorService.shutdown();
}
Redis의 Monitor로 실행명령어를 확인해보자.
1713753984.532783 [0 127.0.0.1:50569] "EXISTS" "1_1_1"
1713753984.532827 [0 127.0.0.1:50569] "EXISTS" "1_1_1"
1713753984.532831 [0 127.0.0.1:50569] "EXISTS" "1_1_1"
.
.
.
1713753984.536772 [0 127.0.0.1:50569] "SET" "1_1_1" "6" //유저 6
1713753984.536955 [0 127.0.0.1:50569] "SET" "1_1_1" "10" //유저 10
1713753984.537118 [0 127.0.0.1:50569] "SET" "1_1_1" "9"
Redis의 monitor를 통해 Redis의 요청되는 모든 명령을 확인한 결과, 단순히 ‘읽기 - 쓰기’ 작업이 Atomic하지 않아서 마지막으로 요청한 유저가 좌석을 선점했다는 사실을 알 수 있다.
그렇다면 Redis의 Lua Script를 통해 Atomic하게 만들었을때 결과는 어떨까?
public void selectSeats(SeatDto seatDto, Integer userId) {
String redisKey = String.format("%d_%d_%d", seatDto.getScheduleId(),
seatDto.getSeatGradeId(), seatDto.getSeatNumber());
String script = "local redisKey = KEYS[1]" +
"local userId = ARGV[1]" +
"redis.call('SET', 'log:before', 'Before setting value in key ' .. redisKey)" +
"if redis.call('EXISTS', redisKey) == 1 then" +
" return 'SEAT_IN_PROGRESS'" +
"else" +
" redis.call('SET', redisKey, userId)" +
" redis.call('EXPIRE', redisKey, 420)" +
" redis.call('SET', 'log:after', 'After setting value in key ' .. redisKey)" +
" return 'SUCCESS'" +
"end";
List<String> keys = Collections.singletonList(redisKey);
String result = redisTemplate.execute(
new DefaultRedisScript<>(script, String.class), keys, String.valueOf(userId));
if ("SEAT_IN_PROGRESS".equals(result)) {
throw new CustomException(ErrorCode.SEAT_IN_PROGRESS);
}
}
‘읽기- 쓰기’ 작업 전/후에 로그를 추가했다. (정확히 말하면 로그가 아니지만 단순히 실행 순서를 확인하기 위해 추가하였으므로 로그라고 하겠다….🥲)
똑같은 조건으로 테스트했을때 Redis Monitor로 확인하면
1713754691.481674 [0 127.0.0.1:51034] "EVALSHA" "a5d7c5cd2d6c9f6e1620df89bf25df76f9b57089" "1" "1_1_1" "2" //유저 2가 lua script 실행
1713754691.481786 [0 lua] "SET" "log:before" "Before setting value in key 1_1_1" //Before 로그
1713754691.481794 [0 lua] "EXISTS" "1_1_1" //좌석 선점했는지 확인
1713754691.481799 [0 lua] "SET" "1_1_1" "2" //좌석이 선점되어있지 않기때문에 좌석 선점
1713754691.481809 [0 lua] "EXPIRE" "1_1_1" "420" //TTL
1713754691.481814 [0 lua] "SET" "log:after" "After setting value in key 1_1_1" //After 로그
1713754691.481822 [0 127.0.0.1:51034] "EVALSHA" "a5d7c5cd2d6c9f6e1620df89bf25df76f9b57089" "1" "1_1_1" "1" //유저 1이 lua script 실행
1713754691.481830 [0 lua] "SET" "log:before" "Before setting value in key 1_1_1"//Before 로그
1713754691.481834 [0 lua] "EXISTS" "1_1_1" //좌석 선점되었는지 확인 선점되어있기때문에 끝.
1713754691.481840 [0 127.0.0.1:51034] "EVALSHA" "a5d7c5cd2d6c9f6e1620df89bf25df76f9b57089" "1" "1_1_1" "3"
1713754691.481846 [0 lua] "SET" "log:before" "Before setting value in key 1_1_1"
1713754691.481850 [0 lua] "EXISTS" "1_1_1"
1713754691.481854 [0 127.0.0.1:51034] "EVALSHA" "a5d7c5cd2d6c9f6e1620df89bf25df76f9b57089" "1" "1_1_1" "4"
1713754691.481860 [0 lua] "SET" "log:before" "Before setting value in key 1_1_1"
1713754691.481866 [0 lua] "EXISTS" "1_1_1"
1713754691.481870 [0 127.0.0.1:51034] "EVALSHA" "a5d7c5cd2d6c9f6e1620df89bf25df76f9b57089" "1" "1_1_1" "5"
1713754691.481876 [0 lua] "SET" "log:before" "Before setting value in key 1_1_1"
1713754691.481880 [0 lua] "EXISTS" "1_1_1"
다음과 같이 처음에 좌석을 선점한 유저의 로그가 Before - After 형태로 잘 찍히고, 다음 좌석 선점을 시도한 유저들은 실패했으므로 Before 로그만 찍혔음을 확인할 수 있다.
'Spring' 카테고리의 다른 글
nGrinder와 Pinpoint로 성능테스트 및 개선하기 (3) - Connection Pool 크기 조절 (0) | 2024.05.19 |
---|---|
nGrinder와 Pinpoint로 성능테스트 및 개선하기 (2) - 병목지점 파악 (0) | 2024.05.14 |
nGrinder와 Pinpoint로 성능테스트 및 개선하기 (1) - 환경 구축 (0) | 2024.05.10 |
Redis Lua Script 와 synchronized 간의 성능 비교 테스트 (+ 수정) (0) | 2024.04.15 |
mockito 테스트 시 파라미터에 무엇을 넣어야할까? (0) | 2024.04.04 |