유저는 마일리지를 한번만 입금하려고합니다.
예상하지 못한 이유로 동일한 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 처리하려면 추가적인 조치가 필요했습니다.
스레드 대기 시간은 길면 길수록 Deduplication을 확실하게 할 수 있습니다.
하지만 시간이 길면 클라이언트 입장에서는 답답함을 느낄 수 있습니다.
그렇기 때문에 하나의 로직에서만 비정상 중복 요청을 처리하는 것이 아닌
서비스 전체적으로 복합적인 예외처리가 필요합니다.
예를 들어 일시적으로 초당 클라언트요청이 폭증하면 post요청만 제한을 한다던지
악의적인 목적이 있다면 모니터링을 통해 IP를 차단해버리는 솔루션이 필요합니다.
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입니다.
Distributed Lock 방식으로 분산 락을 이용하는 방법이 있습니다.
금액과 같은 민감한 데이터만 처리하는 서버를 분리해서 성능 향상을 시도합니다.
RabbitMQ같은 메시지큐를 사용하여 복합적으로 처리하는 방법으로 시도할 수 있습니다.
예를 들어 클라이언트가 중복 요청을 보내면 MQ에서 순차적으로 서버로 전달하고
서버는 Redis에 첫번째 요청의 id를 일정시간동안 넣어두고 두번째 중복 요청부터는
Redis를 확인하여 지정된 시간동안의 같은 id는 무시해버릴 수 있습니다.