티스토리 뷰

반응형

사용자가 임의로 서비스에 접근할 때 서버의 처리비용 그리고 DB의 처리비용을 완화하려고 대기열 구현을 하는 것은 흔할 수 있다. 선착순 문제라고 생각하면 쉽다.

대기열 순번을 구현할 때, 순번은 어딘가에 영속적으로 저장되어야하는데 DB와 Redis 두 개를 선택사항을 두고 고민했고, 나는 대기열을 이용할 때 Redis를 활용했다. 

왜냐하면 다음과 같은 이유가 있다.

1. 대기표 순번은 아주 단기간에 몰리는 트래픽을 처리하는데 사용할 필요가 있다. Read, Write 비용이 높다.
2. 피크 타임에 단기간에 몰린 뒤 서비스가 완료된 뒤 그 이후에는 필요가 없다. 즉, DB에 쌓는다면 필요없는 레코드가 수백만건이 쌓일 것이다. 무언가가 배치로 DB Table로 부터 레코드를 지워야한다. 그러나 Redis를 활용한다면 Max-memory 옵션에 따라 알아서 필요없는 키를 제거할 것 이다.(LRU 등의 알고리즘으로)

대기열을 구현할 때 redis의 sorted set을 활용했다. ZADD, ZREM, ZRANK로 순번을 정학히 파악할 수 있기 때문이다. 관련하여 더 정리 잘된 좋은 문서는 redis 공식 문서나 다른 블로그를 참조 부탁합니다. 

순번이 5분간 유효한 점은 다른 key-value set을 활용하여 TTL을 걸었고 랭크를 확인하기전 guard code로 쓸모 없는 숫자를 발생시켰다. (repository layer에서 exception을 발생시키는건 바람직 하지 않으므로)

그리고 ZRANK를 확인한후 제일 처음인 사람인 0번인 것이 확인하면 다음 서비스로 넘어가야하는데, 다음 서비스로 넘어가기전 해당 토큰은 다시 활용할 수 없게 ZREM으로 지우게 하였다.

또 하나 처리한 부분은 redis에서 key-value set은 GETDEL이라는 것을 지원하지만 sorted-set에 대해선 GETDEL이 지원되지 않아 원자적으로 실행할 수 없다는 점이다. 이 부분은 동시성 테스트를 할 때 발급받은 티켓번호를 여러번 재활용할 수 있는 문제가 생길 것이므로 분산락을 활용하여 한 번 대기표를 패스한 번호는 반드시 지울 수 있도록 처리하였다.

분산락을 활용한 이유는 다음과 같다: 어차피 redis layer를 접근하므로 redis에서 구현하는 것이 좋은 선택이라고 생각했기 때문이다.

코드는 다음과 같다.

    @Override
    @DistributedLock(key = "#token")
    public long readAndDeleteWaitingNumber(String token) {
        String value = this.valueOperation.getAndDelete(token);
        if (value == null) {
            return -1L;
        }
        Long rank = this.zSetOperations.rank(rankKey,token);
        if (rank == 0){
            this.zSetOperations.remove(rankKey, token);
        }
        return rank;
    }

테스트코드는 다음과 같이 작성하였다.

    @Test
    void test_대기표_읽은후_삭제확인_동시성테스트() {

        ticketWriterRepository.writeNewTicket("test", 1);
        int numThreads = 60;

        ExecutorService executorService = Executors.newFixedThreadPool(60);
        List<CompletableFuture> futures = new ArrayList<>();

        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger failCount = new AtomicInteger(0);

        for (int i=1; i<= numThreads; i++) {
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                Long res = ticketReaderRepository.readAndDeleteWaitingNumber("test-1");
                if (res == 0) successCount.incrementAndGet();
                else failCount.incrementAndGet();
            }, executorService);
            futures.add(future);
        }

        CompletableFuture.allOf((futures.toArray(new CompletableFuture[0]))).join();
        executorService.shutdown();

        Assertions.assertThat(successCount.get()).isEqualTo(1);
        Assertions.assertThat(failCount.get()).isEqualTo(59);
    }

분산락 구현과 관련하여 좋은 문서는 다음이 있다.

https://helloworld.kurly.com/blog/distributed-redisson-lock/

 

문서 끝.

반응형