동시성 처리하기 1

JaeGu Jeong·2024년 2월 26일
0

동시성 처리하기

목록 보기
1/2
post-thumbnail

상황

유저는 마일리지를 한번만 입금하려고합니다.
예상하지 못한 이유로 동일한 1만개의 입금 요청이 동시에 API에게 들어갑니다.
API는 처음 요청만 처리하고 나머지는 400 상태 코드로 처리하고자 합니다.
즉, 1개의 201 status와 9999개의 400 status를 요구합니다.

조건

API로 들어오는 값은 User Id와 Mileage만 주어집니다.

접근방법

처음에는 synchronized로 락을 걸고 Static Set으로 id를 등록해서
이미 진행중인 id면 추가 요청을 막으려고 하였습니다.
id를 등록하고 들어오는 요청은 400처리가 가능하지만 Static Set에 id를 등록하는 동안
임계구역에 대기하고 있는 요청을 400 처리하려면 추가적인 조치가 필요했습니다.

  1. 일단 임계구역에 들어온 첫번째 요청은 id를 Static Set에 등록하고 임계구역을 나옵니다.
  2. 첫번째 요청은 입금 처리를 시작합니다.
  3. 임계구역에 접근하지 않은 새로운 요청의 id가 이미 Set에 있다면 400 처리합니다.
  4. 입금 처리가 끝나면 첫번째 요청만 스레드 대기합니다.
  5. 첫번째 요청의 스레드 대기 시간동안 임계구역에서 대기중인 요청을 모두 400 처리합니다.
  6. 클라이언트에게 첫번째 요청의 입금이 완료됨을 알립니다.

스레드 대기 시간

스레드 대기 시간은 길면 길수록 Deduplication을 확실하게 할 수 있습니다.
하지만 시간이 길면 클라이언트 입장에서는 답답함을 느낄 수 있습니다.
그렇기 때문에 하나의 로직에서만 비정상 중복 요청을 처리하는 것이 아닌
서비스 전체적으로 복합적인 예외처리가 필요합니다.
예를 들어 일시적으로 초당 클라언트요청이 폭증하면 post요청만 제한을 한다던지
악의적인 목적이 있다면 모니터링을 통해 IP를 차단해버리는 솔루션이 필요합니다.

Java Code

private final Database db;
private final static Set<Long> depositingUsers = ConcurrentHashMap.newKeySet();

public Mileage postMileage(Long id, Long mileage) {
    Mileage result;
    if (depositingUsers.contains(id)){
        return null;
    }
    synchronized (this) {
        if (depositingUsers.contains(id)) {
            return null;
        }
        depositingUsers.add(id);
    }
    try {
        result = db.postMileage(id, mileage);
        Thread.sleep(4000L);
    } catch (Exception e){
        //err logger
        return null;
    } finally {
        depositingUsers.remove(id);
    }
    return result;
}

동시성 테스트 기록

테스트 방식은 ExecutorService와 CountDownLatch를 사용하여 테스트 하였습니다.
10만개까지 늘리려 하였으나 스레드 2만5천개부터 생성만해도 PC에 100% 과부하가 걸려 테스트가 어려웠습니다.

측정 시간은 스레드를 모두 생성 후 Latch를 풀기 직전부터 스레드가 모두 종료 순간까지 입니다.
오차범위는 500ms입니다.

요청 10000개씩 2명 테스트 성공 / 5초

요청 1000개씩 20명 테스트 성공 / 5초

요청 100개씩 200명 테스트 성공 / 5초

요청 5개씩 4000명 테스트 성공 / 12초

요청 3개씩 8000명 테스트 성공 / 37초

요청 2개씩 15000명 테스트 성공 / 1분30초

응답시간 개선 방안

Distributed Lock 방식으로 분산 락을 이용하는 방법이 있습니다.
금액과 같은 민감한 데이터만 처리하는 서버를 분리해서 성능 향상을 시도합니다.
RabbitMQ같은 메시지큐를 사용하여 복합적으로 처리하는 방법으로 시도할 수 있습니다.
예를 들어 클라이언트가 중복 요청을 보내면 MQ에서 순차적으로 서버로 전달하고
서버는 Redis에 첫번째 요청의 id를 일정시간동안 넣어두고 두번째 중복 요청부터는
Redis를 확인하여 지정된 시간동안의 같은 id는 무시해버릴 수 있습니다.

profile
BackEnd Developer

0개의 댓글