[Spring Boot] 선착순 이벤트 쿠폰을 만들어보자 (1) - 동시성 이슈

들어가기 전에 우아한테크톡의 선착순 이벤트 서버 생존기를 보고 아이디어를 얻었고, 그 과정을 처음부터 부딪히면서 배우기 위해 바닥부터 만들어보면서 느낀점과 배운것들을 정리하기 위

tmd8633.tistory.com

 

[Spring Boot] 선착순 이벤트 쿠폰을 만들어보자 (2) - Redis

[Spring Boot] 선착순 이벤트 쿠폰을 만들어보자 (1) - 동시성 이슈들어가기 전에 우아한테크톡의 선착순 이벤트 서버 생존기를 보고 아이디어를 얻었고, 그 과정을 처음부터 부딪히면서 배우기 위

tmd8633.tistory.com

이전 글을 읽어보시기 바랍니다. 이전 글에서 이어집니다.

 

 

개요

이전 글에서 선착순 이벤트 쿠폰 발급에 대해서 공부했습니다. 이는 오늘 같이 공부해볼 내용을 하기 위해서 선행학습에 불과합니다. 파트별로 공부하기 위해서 구현해보았던 것이죠. 오늘은 이벤트 주문에 대해서 공부해볼겁니다.

 

큐넷

큐넷 시험신청이나 수강신청을 할 때 이런 대기열을 보셨을겁니다. 이걸 만들어볼겁니다. 이전 글에서 Redis를 활용하여 대기열을 만들었었는데 그걸 이용하면 금방 구현해볼 수 있을 것같습니다.

 

 

이전과 다른 점

이전 선착순 이벤트 쿠폰을 발행하는 로직은 줄을 세우고 순차적으로 원하는 양만큼만 발급해주면 끝났습니다. 그런데 이번에는 줄을 세우고 줄을 선 유저에게 자신이 몇번째인지 알려주고 이벤트 페이지에 접속하기전까지 갱신해주어야합니다. 이 점을 생각하면서 전체적인 구조를 그려보겠습니다.

 

 

구조

유저가 /event 에 접속 요청을 하면 Filter에서 인증된 토큰이 있는지 검사합니다. 없으면 Redis에서 토큰을 생성해 대기열에 넣고, /waiting 으로 redirect 해서 현재 대기열을 표시해줍니다.

/waiting 에서 /api/v1/polling 요청으로 현재 대기열이 몇명인지, 인증되었는지, 내 대기순번은 몇번인지를 1초마다 polling 해줍니다.

유저의 토큰이 인증되었으면 /event 로 보내줍니다.

 

이렇게 구성해보았습니다. 이제 구현해보겠습니다.

 

 

 

Page

@Controller
public class EventController {

    @GetMapping("/")
    public String mainPage() {
        return "main";
    }

    @GetMapping("/event")
    public String orderPage() {
        return "event";
    }

    @GetMapping("/waiting")
    public String waitingPage() {
        return "waiting";
    }

}

 

main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>메인페이지임</title>
</head>
<body>
    <h1>메인페이지임</h1>
    <a href="/event">이벤트 참여</a>
</body>
</html>

메인페이지에서 이벤트를 참여할 수 있게 만들었습니다.

event.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>이벤트 페이지</title>
</head>
<body>
    <h1>이벤트 페이지임</h1>
</body>
</html>

이벤트페이지입니다. 인증된 토큰이 있어야지 접속할 수 있습니다.

 

waiting.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>대기중</title>
    <style>
        body {
            width: 100%;
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .container {
            width: 300px;
            height: 400px;
            background-color: antiquewhite;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        h1,
        h3 {
            text-align: center;
        }
    </style>
    <script src="/js/waiting.js"></script>
</head>
<body>
<div class="container">
    <div id="card">
        <h3>대기번호</h3>
        <h1 id="rank">0</h1>
        <span>뒤에 <span id="waitingCount">0</span>명이 있습니다.</span>
    </div>
</div>
</body>
</html>

대기열페이지입니다. 내 대기번호와 그 뒤로 몇명의 대기자기 있는지 확인할 수 있습니다.

 

waiting.js

const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');

if (token == null) {
    alert('토큰 없음');
    window.location.href = '/';
}

window.addEventListener('load', () => {

    const rank = document.querySelector('#rank');
    const remain = document.querySelector('#waitingCount');

    const poling = () => setInterval(() => fetchGet('/api/v1/poling', callback), 1000);
    poling();


    const callback = (json) => {
        let jRank = json.rank;
        let jTotal = json.total;
        let jAuthenticated = json.authenticated;

        if (jAuthenticated) {
            location.href = `/event?token=${token}`;
            return;
        }

        if (jRank == null) {
            alert('토큰 정보가 없습니다.');
            location.href = '/';
            return;
        }
        rank.innerHTML = jRank + '';
        remain.innerHTML = (Number(jTotal) - Number(jRank)) + '';
    }
});

function fetchGet(url, callback) {
    fetch(`${url}?token=${token}`)
        .then(response => response.json())
        .then(json => callback(json))
}

 

setInterval로 1000ms 마다 서버에서 대기번호, 전체대기열사이즈, 인증여부를 받아옵니다.

인증이 되었다면 인증된 토큰과 함께 /event 페이지로 이동합니다.

 

 

Dto

public record ResponseToken(
    boolean authenticated,	// 인증여부
    Long total, 		// 전체 대기열 수
    Long rank) {		// 대기번호

}

 

Redis

@Service
@RequiredArgsConstructor
public class TokenRedisService {

    private final TokenRedisRepository tokenRedisRepository;

    private static final String WAITING_KEY = "event:waiting";
    private static final String AUTHENTICATED_KEY = "event:authenticated";

    @PostConstruct
    void initDeleteAll() {
        tokenRedisRepository.delete(WAITING_KEY);
        tokenRedisRepository.delete(AUTHENTICATED_KEY);
    }

    public String register() {
        String token = UUID.randomUUID().toString();
        tokenRedisRepository.addIfAbsent(WAITING_KEY, token);
        return token;
    }

    public boolean isAuthenticated(String token) {
       return tokenRedisRepository.exists(AUTHENTICATED_KEY, token);
    }
    public boolean isWaiting(String token) {
        return tokenRedisRepository.exists(WAITING_KEY, token);
    }

    public ResponseToken getRank(String token) {
        Long total = tokenRedisRepository.getSize(WAITING_KEY);
        Long rank = tokenRedisRepository.getRank(WAITING_KEY, token);
        boolean authenticated = tokenRedisRepository.exists(AUTHENTICATED_KEY, token);
        return new ResponseToken(authenticated, total, rank);
    }

    public void authenticate(long poolSize) {
        for (long i = 0; i < poolSize; i++) {
            String token = tokenRedisRepository.popMinIfExists(WAITING_KEY);
            if (token == null) return;

            tokenRedisRepository.addIfAbsent(AUTHENTICATED_KEY, token);
        }
    }

}

 

@Repository
@RequiredArgsConstructor
public class TokenRedisRepository {

    private final StringRedisTemplate redisTemplate;

    public Long getSize(String key) {
        return redisTemplate.opsForZSet().size(key);
    }

    public void delete(String key) {
        redisTemplate.delete(key);
    }

    public String popMinIfExists(String key) {
        if (getSize(key) < 1) return null;
        var tuple = redisTemplate.opsForZSet().popMin(key);
        return (tuple == null) ? null : tuple.getValue();
    }

    public Boolean addIfAbsent(String key, String token) {
        return redisTemplate.opsForZSet().addIfAbsent(
            key,
            token,
            System.currentTimeMillis()
        );
    }

    public void removeToken(String key, String token) {
        redisTemplate.opsForZSet().remove(key, token);
    }

    public boolean exists(String key, String token) {
        return redisTemplate.opsForZSet().score(key, token) != null;
    }

    public Long getRank(String key, String token) {
        Long rank = redisTemplate.opsForZSet().rank(key, token);
        return rank != null ? rank + 1 : null;
    }

}

 

 

 

Filter

@Configuration
@RequiredArgsConstructor
public class FilterConfig implements WebMvcConfigurer {

    private final EventTokenFilter eventTokenFilter;

    @Bean
    FilterRegistrationBean<EventTokenFilter> filterRegistrationBean() {
        FilterRegistrationBean<EventTokenFilter> filter = new FilterRegistrationBean<>(eventTokenFilter);
        filter.setOrder(1);
        filter.addUrlPatterns("/event/*");
        return filter;
    }

}
@Component
@RequiredArgsConstructor
public class EventTokenFilter extends OncePerRequestFilter {

    private final TokenRedisService tokenRedisService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = request.getParameter("token");

        if (token == null) {
            token = tokenRedisService.register();
            response.sendRedirect("/waiting?token=" + token);
            return;
        }

        boolean isAuthenticated = tokenRedisService.isAuthenticated(token);

        if (!isAuthenticated) {
            response.sendRedirect("/waiting?token=" + token);
            return;
        }

        filterChain.doFilter(request, response);
    }

}

 

/event 요청의 Filter입니다.

  1. token이 없으면 토큰을 생성해서 대기열에 넣고 /waiting 으로 redirect 합니다.
  2. token이 있지만 인증된 토큰이 아니라면 /waiting 으로 redirect 합니다.

 

 

Polling

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
public class EventRestController {

    private final TokenRedisService tokenRedisService;

    @GetMapping("/poling")
    public ResponseEntity<ResponseToken> rank(String token) {
        ResponseToken rToken = tokenRedisService.getRank(token);
        return ResponseEntity.ok(rToken);
    }

}

 

 

 

Schedule

@Configuration
@RequiredArgsConstructor
public class EventScheduler {

    private final TokenRedisService tokenRedisService;
    
    private static final long POOL_SIZE = 100;

    @Scheduled(fixedDelay = 1000)
    public void eventScheduler() {
        tokenRedisService.authenticate(POOL_SIZE);
    }
}

 

1초마다 대기열에서 100명의 유저를 인증해줍니다.

 

 

 

결과

JMeter로 대기열에 사람을 채우면서 대기열에 진입해보겠습니다.

 

 

/waiting
/waiting
/event 페이지

 

정상 동작합니다. 영상으로 남기고싶었는데 귀찮아서 사진으로 그냥 찍었습니다. 궁금하면 만들어서 직접 해보세요

 

 

 

 

추가

여기까지만 하려는데 조금 아쉽습니다.

 

첫번째로 위의 코드는 고정적인 유저수를 1초간격으로 허용해주는데 내가 원하는 사이즈만큼만 허용시키고싶은 생각이 들더라구요 전체 허용시킬 size가 1000이고, 1초마다 100명의 유저를 인증해준다고했을때 전체 size 1000이 다 차면 더이상 허용시키지 않고 인증된 유저가 페이지를 벗어나면 그 수만큼 인증시키고싶었습니다.

 

두번째는 대기열에서 유저가 빠져나가면 대기열 토큰을 지워 불필요한 데이터가 담기지 않게 하고싶었습니다.

 

이 두 작업을 더해보겠습니다.

 

먼저 대기열에서 토큰을 지워주는 /api/v1/leave와 인증된토큰을 지워주는 /api/v1/exit를 만들어보겠습니다.

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
public class EventRestController {

    private final TokenRedisService tokenRedisService;

    @GetMapping("/poling")
    public ResponseEntity<ResponseToken> rank(String token) {
        ResponseToken rToken = tokenRedisService.getRank(token);
        return ResponseEntity.ok(rToken);
    }

    @PostMapping("/exit")
    public ResponseEntity<String> exit(String token) {
        tokenRedisService.exit(token);
        return ResponseEntity.ok("exit");
    }

    @PostMapping("/leave")
    public ResponseEntity<String> leave(String token) {
        tokenRedisService.leave(token);
        return ResponseEntity.ok("leave");
    }
}

 

TokenRedisService

    public void leave(String token) {
        tokenRedisRepository.removeToken(WAITING_KEY, token);
    }

    public void exit(String token) {
        tokenRedisRepository.removeToken(AUTHENTICATED_KEY, token);
    }

 

 

1. 대기열 토큰 제거

window.addEventListener('beforeunload', () => {
    navigator.sendBeacon(`/api/v1/leave?token=${token}`);
})

waiting.js 에서 unload 되기전에 /api/v1/leave 에 토큰을 보내 대기열에서 제거해주도록 했습니다.

 

 

 

2. 인증토큰 제거

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>이벤트 페이지</title>
</head>
<body>
    <h1>이벤트 페이지임</h1>
    <button>완료</button>

    <script>
        const button = document.querySelector('button');

        const param = new URLSearchParams(window.location.search);
        const token = param.get('token');

        button.addEventListener('click', () => {

            fetch(`/api/v1/exit?token=${token}`, {method : 'POST'})
                .then(res => {
                    window.location.href = '/';
                })
        })

    </script>
</body>
</html>

이벤트 페이지에서 모든 작업이 완료되었다면 /api/v1/exit 요청으로 인증된 토큰을 제거해줍니다.

 

 

TokenRedisService

    public void authenticateWithStaticPool(long poolSize) {
        for (long i = 0; i < poolSize; i++) {
            String token = tokenRedisRepository.popMinIfExists(WAITING_KEY);
            if (token == null) return;

            tokenRedisRepository.addIfAbsent(AUTHENTICATED_KEY, token);
        }
    }

    public void authenticateWithMaxPool(long maxPoolSize, long poolSize) {
        Long authenticatedSize = tokenRedisRepository.getSize(AUTHENTICATED_KEY);
        long remainAuthenticatedSize = Math.min(maxPoolSize - authenticatedSize, poolSize);
        System.out.println("현재 인증된 토큰 수 : " + authenticatedSize + ", 인증 가능한 수 : " + remainAuthenticatedSize);
        authenticateWithStaticPool(remainAuthenticatedSize);
    }

 

기존의 authenticate 를 위와 같이 변경해줍니다. authenticateWithStaticPool 은 기존의 authenticate이고,

authenticateWithMaxPool 은 최대 인증토큰 개수까지만 인증시키는 메소드입니다.

 

@Configuration
@RequiredArgsConstructor
public class EventScheduler {

    private final TokenRedisService tokenRedisService;

    private static final long POOL_SIZE = 100;
    private static final long MAX_POOL_SIZE = 1000;

    @Scheduled(fixedDelay = 1000)
    public void eventScheduler() {
        tokenRedisService.authenticateWithMaxPool(MAX_POOL_SIZE, POOL_SIZE);
    }
}

이렇게 하고 실행해봅시다.

 

 

결과

100 개씩 인증하다가 최대 사이즈가 다 차면 인증토큰을 더이상 발행하지 않습니다.

이때 1001번 대기자는 계속 polling 하면서 대기하다가 앞에서 인증토큰이 exit되면

1001번 대기자 이벤트페이지 접속

들어오는 걸 확인할 수 있습니다.

1001번째 대기자 polling되었을때의 Schedule

 

아직 해결못한 내용

이렇게 했을 때 아직 해결못한것이 있습니다. 인증된 토큰을 어떻게 지우냐 입니다. 위의 코드는 인증된 토큰을 지우려면 이벤트페이지에서 어떤 작업을 모두 완료했을때 뿐입니다.

만약 중간에 페이지를 닫거나 새로운 페이지로 이동한다면 어떻게 토큰을 지워야할까요?

JS에서 beforeunload 로 토큰을 지우도록 하면 다음 문제는 /event 페이지에서 /event/** 로 이동할때 발생합니다. 페이지가 변경되면 인증된 토큰이 사라지거든요.

이 문제에 대해서는 해결하면 글써보도록하겠습니다.

 

 

[Spring Boot] 선착순 이벤트 쿠폰을 만들어보자 (1) - 동시성 이슈

들어가기 전에 우아한테크톡의 선착순 이벤트 서버 생존기를 보고 아이디어를 얻었고, 그 과정을 처음부터 부딪히면서 배우기 위해 바닥부터 만들어보면서 느낀점과 배운것들을 정리하기 위

tmd8633.tistory.com

 

이전 글에서 이어집니다.

 

 

 

개요

저번 글에서 동시성이슈를 해결했었습니다. 이번에는 순간적인 대량의 트래픽을 감당하기 위해 TPS를 높혀보겠습니다.

우아한테크톡에서도 보았듯이 Redis를 통해 이 문제를 해결해 보도록 하겠습니다.

 

 

 

 

구조

 

  1. 유저가 이벤트 쿠폰 발행을 요청하면 Redis에서 SortedSet으로 정렬해서 대기열에 넣습니다.
  2. Schedule로 1초간격으로 한정된 유저만큼 쿠폰 발급을 요청합니다.
  3. /api/v3/status 로 발급이 완료되었는지 확인할 수 있습니다.

 

이런 순서로 동작하도록 설계했습니다. 여기서 중요한 것은 많은 트래픽을 감당하기 위해서 대기열에 진입하는 로직에서 무거운 작업을 빼서 최소화 하기로했습니다. 

 

 

CouponConfig

@Configuration
@RequiredArgsConstructor
public class CouponConfig {

    private final JpaCouponRepository jpaCouponRepository;
    public static final int COUPON_COUNT = 200;

    @PostConstruct
    public void init() {
        Coupon saveCoupon = Coupon.builder()
            .name("한정수량 쿠폰")
            .amount(COUPON_COUNT)
            .build();
        jpaCouponRepository.save(saveCoupon);
    }
}

쿠폰 발급갯수를 public static으로 만들어놓았습니다.

 

 

CouponController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v3")
public class CouponControllerV3 {

    private final CouponRedisService couponRedisService;

    @PostMapping("/coupon")
    public ResponseEntity<ResponseCouponStatus> coupon() {
        long userId = Math.abs(new Random().nextLong() % 2000) + 1;
        boolean register = couponRedisService.register(userId);

        return ResponseEntity.ok(new ResponseCouponStatus(register ? "요청" : "마감", userId));
    }

    @GetMapping("/status")
    public ResponseEntity<Boolean> status(Long userId) {
        boolean isIssued = couponRedisService.isIssued(userId);
        return ResponseEntity.ok(isIssued);
    }

}

/api/v3/coupon 은 쿠폰 발급 요청 API

/api/v3/status 는 쿠폰 발급이 완료되었는지를 확인하는 API 입니다.

boolean register는 대기열에 정상적으로 등록되었으면 true, 등록이 되지않았으면(이미 마감된경우) false를 반환합니다.

userId 를 Response에 담아서 보내는 이유는 테스트를 위함입니다. 랜덤으로 userId를 만들었기때문에 랜덤으로 만들어진 userId로 /api/v3/status 로 발급여부를 확인하기 위해 다시 한번 요청을 해야하기때문입니다.

 

CouponRedisService

@Service
@RequiredArgsConstructor
public class CouponRedisService {

    private final CouponRedisRepository couponRedisRepository;

    private static final String WAITING_QUEUE_KEY = "coupon:waiting";     // 대기열
    private static final String ISSUED_SET_KEY = "coupon:issued";   // 처리된 유저 목록
    private boolean isFull = false;

    @PostConstruct
    void init() {
        couponRedisRepository.delete(WAITING_QUEUE_KEY);
        couponRedisRepository.delete(ISSUED_SET_KEY);
    }

    public boolean register(Long userId) {
        if (isFull) return false;
        return addIfAbsent(WAITING_QUEUE_KEY, String.valueOf(userId));
    }

    public List<String> getWaitingUsers() {
        if (isFull) return null;
        return addIssued();
    }

    private Long getSize(String key) {
        return couponRedisRepository.getSize(key);
    }

    private List<String> addIssued() {
        List<String> temp = new ArrayList<>(COUPON_COUNT);

        String userId = popMinIfExists(WAITING_QUEUE_KEY);

        while (userId != null) {
            if (getSize(ISSUED_SET_KEY) >= COUPON_COUNT) {
                isFull = true;
                couponRedisRepository.delete(WAITING_QUEUE_KEY);
                return temp;
            }
            Boolean isAdded = addIfAbsent(ISSUED_SET_KEY, userId);

            if (Boolean.TRUE.equals(isAdded)) {
                temp.add(userId);
            }

            userId = popMinIfExists(WAITING_QUEUE_KEY);
        }
        return temp;
    }

    private String popMinIfExists(String key) {
        return couponRedisRepository.popMinIfExists(key);
    }

    private Boolean addIfAbsent(String key, String userId) {
        return couponRedisRepository.addIfAbsent(key, userId);
    }

    public boolean isIssued(long userId) {
        return couponRedisRepository.existsByUserId(ISSUED_SET_KEY, userId);
    }
}

 

코드를 분리한게 많아서 하나하나 설명하겠습니다.

public 메소드는 register, getWaitingUsers, isIssued 뿐입니다.

register는 Redis로 대기열에 추가하는 메소드,

getWaitingUsers는 Schedule에서 대기열의 유저를 받아오는 메소드로 DB에 이벤트쿠폰을 넣어주는 역할을 합니다.

isIssued는 유저가 발급되었는지 확인하는 메소드로 /api/v3/status 요청에 사용됩니다.

 

COUPON_COUNT 는 import static 된 데이터입니다. CouponConfig에 COUPON_COUNT를 말합니다.

getWaitinUers는 ISSUED_SET_KEY size를 확인하고 최대 남은 발급수량만큼 대기열에서 유저를 pop 해줍니다.

 

 

CouponRedisRepository

@Repository
@RequiredArgsConstructor
public class CouponRedisRepository {

    private final StringRedisTemplate redisTemplate;

    public Long getSize(String key) {
        return redisTemplate.opsForZSet().size(key);
    }

    public void delete(String key) {
        redisTemplate.delete(key);
    }

    public String popMinIfExists(String key) {
        if (getSize(key) < 1) return null;
        var tuple = redisTemplate.opsForZSet().popMin(key);
        return (tuple == null) ? null : tuple.getValue();
    }

    public Boolean addIfAbsent(String key, String userId) {
        return redisTemplate.opsForZSet().addIfAbsent(
            key,
            userId,
            System.currentTimeMillis()
        );
    }

    public boolean existsByUserId(String key, long userId) {
        return redisTemplate.opsForZSet().score(key, String.valueOf(userId)) != null;
    }
}

RedisTemplate를 관리하는 Repository입니다.

 

 

Scheduler

@EnableScheduling
@Configuration
@RequiredArgsConstructor
public class CouponScheduler {

    private final CouponRedisService couponRedisService;
    private final CouponServiceV3 couponServiceV3;

    @Scheduled(fixedDelay = 1000)
    public void processCouponRequests() {

        var waitingUsers = couponRedisService.getWaitingUsers();

        if (waitingUsers == null || waitingUsers.isEmpty()) return;

        couponServiceV3.issueEventCoupons(waitingUsers);
    }
}

대기중인 유저를 가져와 saveAll 하는 메소드입니다. 작업이 1초 이상이 걸릴 수 있기 때문에 fixedDelay를 사용했습니다.

 

CouponServiceV3

@Service
@RequiredArgsConstructor
public class CouponServiceV3 {

    private final JpaCouponRepository jpaCouponRepository;
    private final CouponRepository couponRepository;


    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public boolean issueEventCoupons(List<String> waitingUsers) {
        Coupon coupon = jpaCouponRepository.findByIdWithLock(1L)
            .orElseThrow(() -> new CouponException("쿠폰을 찾을 수 없습니다."));
        if (!coupon.isAvailable()) {
            return false;
        }

        coupon.decreaseAmount(waitingUsers.size());

        List<EventCoupon> saveEventCoupons = waitingUsers
            .stream()
            .map(x -> EventCoupon.builder()
                .userId(Long.valueOf(x))
                .createdAt(LocalDateTime.now())
                .coupon(coupon)
                .build())
            .toList();

        couponRepository.saveEventCoupons(saveEventCoupons);

        return coupon.isAvailable();
    }
}

대기중인 유저들에게 EventCoupon saveAll 하는 역할을 합니다.

 

 

결과

400 Thread, 10초간 요청하도록 설정

환경은 이전과 동일합니다.

 

 

Samples는 2.5배, 평균응답시간(ms) 2.8배, TPS 2.5배 향상이 있었습니다.

 

응답스펙이 Redis가 30 bytes가 높아서 그렇지 스펙을 똑같이 맞춘다면 어마어마한 속도차이가 있습니다.

Samples는 4.4배, 평균응답시간(ms) 4.8배, TPS 4.2배 빨랐습니다.

 

 

결론

우연히 우아한테크톡에서 봤던 영상덕분에 이벤트 쿠폰을 발행하는 과정에서 생길 수 있는 문제 (동시성, 트래픽처리)에 대해서 배울 수 있었던 것같습니다. Lock 에 대해서도 공부가 되었고, Redis가 아직 익숙하지 않지만 더 공부해서 잘 사용하게 될때쯤 글을 적어보려고합니다.  이게 글로는 엄청 짧아보이는데 Redis를 사용해 어떻게 해야 조금 더 빨라 질 수 있을까에 대해서 고민하면서 코드를 지우고 적고 하면서 배운걸 적다보니 결론만 내린 느낌같습니다. 그런데 지금 위에 코드를 보면 변경하고싶은 사항들이 많아보입니다. 대기열에서 발급받은유저목록으로 보내는 과정에서 조금더 최적화 할 수 있을 것같거든요. 하지만 더이상 손대지 않을겁니다. 이번 실기나 이벤트 쿠폰 발급은 메인이 아니거든요. 다음 글 부터는 대기열을 이용해 rank를 반환해주고 다음 과정을 넘어가기 전에 buffer를 만들어볼까합니다.

큐넷

이런 기능처럼요. 아직 해보지 않아서 며칠 걸릴 수 있습니다. 

+ Recent posts