Skip to content

선착순 ‐ 동시성 문제

mjmj edited this page Aug 25, 2024 · 5 revisions

선착순

colab 주소

단순 db로만 카운팅 한 경우

문제점

  • 동시 접속자가 많아짐에 따라 race condition이 발생함
    • 당첨자 1000명 뽑는 이벤트에 당첨자가 1700명 저장되고 있음
image
  • 성능
    • 최대 40 rps가 나옴
    • 시간이 지날수록 response time이 증가함
    • cpu 사용량이 낮음 (application 자원은 별로 사용하지 않음)
      • 스레드 풀 수를 늘려보는건 어떨까?
    • db 병목이 매우 심하다.
      • 히카리 풀을 늘려보는 건?
      • 결국 redis..?
    • 요청마다 당첨자 수를 increase 하며 데이터베이스 네트워크 비용이 매우 심함

시나리오

  • 동시접속자 1000명, 0 ~ 1000명까지 초당 10명씩 늘어날 때

유저 테이블에 10만 row가 있는데 JWT가 들어올 때마다 유저 정보를 이메일을 통해서 조회한다. 이 과정에서 10만 row를 fullscan을 하면서 성능이 매우 좋지 않았다.

email key에 인덱스를 걸어 이 문제를 해결하였다.

User 테이블에 인덱스 안걸었을 때.. image

image

User 테이블 인덱스 걸었을 때 image

image

동접자 800명이 넘어가는 순간부터 응답 시간이 증가함

  • threaddump 확인해보자

  • 대기 큐 확인 필요할지도..

  • db 접근이 너무 많나..?

  • 동시 접속자 0 ~ 700명, 1초에 10명씩 증가

image image

동기화 문제 해결

  1. @Transactionalsynchronized 동시 사용 - 해결 실패 ㅠ
    @Transactional
    public synchronized QuizFirstComeSubmitResponseDto quizSubmit(QuizFirstComeSubmitRequest quizFirstComeSubmitRequest, User authenticatedUser) {

        Long subEventId = quizFirstComeSubmitRequest.getSubEventId();
        String answer = quizFirstComeSubmitRequest.getAnswer();

        SubEvent subEvent = subEventRepository.findById(subEventId)
                .orElseThrow(() -> new SubEventNotFoundException());

        QuizFirstCome quizFirstCome = quizFirstComeRepository.findBySubEventId(subEventId)
                .orElseThrow(() -> new SubEventNotFoundException());

        if (dateUtil.isNotWithinSubEventPeriod(subEvent)) {
            throw new SubEventNotWithinPeriodException();
        }

        if (!quizFirstCome.getAnswer().equals(answer)) {
            return QuizFirstComeSubmitResponseDto.notCorrect();
        }

        EventUser eventUser = EventUser
                .builder()
                .user(authenticatedUser)
                .subEvent(subEvent)
                .chance(-1)
                .build();

        eventUserRepository.save(eventUser);

        return quizWinnerDraw.winnerDraw(quizFirstCome, subEvent, authenticatedUser);
    }
  • @Transactional을 사용한다는 것은 AOP를 사용하여 프록시를 호출하는 것이다.
  • proxy가 동기화 메서드를 모두 끝낸 후 transaction이 커밋되기까지 시간 차이가 생김 이 사이에 다른 스레드가 동기화 메서드에 접근한다.
  • 즉, 데이터가 commit 되기전에 다른 스레드가 이 데이터베이스를 접근한다.
  • synchronized는 완벽하게 동작하지 않음
  1. 선착순 당첨자 로직에만 synchronized 사용 - 실패..
  • QuizFirstCome이 winnerCount를 가지고 있는데 이 엔티티를 가져오는 시점은 당첨자 로직이 시작되기 전(동기화 x)이기 때문에 동시성 문제를 해결할 수 없었음.
  1. QuizFirstCome에서 당첨자 수를 관리하지 않고 직접 쿼리를 보내 Winners 엔티티의 당첨자 수를 직접 세줌
    @Query("SELECT COUNT(w) FROM Winner w WHERE w.subEvent.id = :subEventId")
    long countWinnerBySubEventId(@Param("subEventId") Long subEventId);
  • @Transactional 메서드 안에서 @Transactional이 없는 메소드를 호출하면 부모 메서드의 트랜잭션이 전파된다.
  • 참고 image
  • 이 또한 @Transactional 문제에서 벗어날 수 없었음
  1. lua script 사용
  • 1,2,3번은 여전히 동시성 문제가 해결되지 않음.
  • 추후 배치 작업 (redis에 당첨자 저장 후) 위해 lua script 사용
  • redis가 싱글스레드에서 동시성을 보장해주기 때문에 문제 없이 동시성을 보장할 수 있었음