Lucian Log
Blog
Computer Science
Algorithm
DB
Network
OS
General
AI
Blockchain
Concurrency
ETC
Git
Infrastructure
AWS
Docker
Java-Ecosystem
JPA
Java
Spring
JavaScript-Ecosystem
JavaScript
Next.js
React
TypeScript
Python-Ecosystem
Django
FastAPI
Python
SQLAlchemy
Software Engineering
Architecture
Culture
Test
Home
Contact
Copyright © 2024 |
Yankos
Home
>
General
> Concurrency
Now Loading ...
Concurrency
선착순 이벤트 시스템 개발하기
선착순 이벤트 시스템 개발하기 발생할 수 있는 문제 쿠폰을 100개만 발급해야 하는데, 쿠폰이 100개보다 많이 발급되었다 트래픽이 급증해 이벤트 페이지 접속이 안된다 이벤트랑 전혀 상관없는 페이지들도 느려졌다 해결책 트래픽이 몰렸을 때 대처하는 방법 적용 Redis를 사용해 쿠폰 발급 개수를 보장하기 Kafka를 활용해 다른 페이지에 미치는 영향을 줄이기 Redis로 Race Condition 해결하기 기존 락 활용의 문제 선착순 쿠폰 발행은 쿠폰 개수에 대한 정합성을 요구함 락 활용은 요구사항의 임계영역이 길어서 성능 불이익 발생 발급된 쿠폰 개수를 가져오는 것부터 쿠폰 생성까지 락을 걸면 임계영역이 길어져서 성능 불이익 해결책: 레디스는 싱글 스레드로 동작해 Race Condition 해결 애플리케이션의 모든 스레드는 언제나 최신 값을 가지게 됨 레디스 incr 명령어 활용 key의 value를 1씩 증가시킴 성능이 매우 빠른 명령어 남은 문제점 쿠폰 발급 개수가 많아질수록 RDB에 부담을 주어 서비스 지연 및 오류 발생 짧은 시간 내 많은 요청 -> DB 서버 리소스 과부하 쿠폰 전용 DB가 아니라면 다른 서비스에도 영향 e.g. MySQL이 1분에 100개 Insert가 가능하다고 가정 10:00 쿠폰 생성 10000개 요청 10:01 주문 생성 요청 10:02 회원가입 요청 -> 쿠폰 생성으로 인해 100분 이후에 주문 및 회원가입 요청이 처리됨 -> 심지어 보통은 타임아웃이 있으므로, 쿠폰 생성 일부분과 주문 및 회원가입 처리 실패 Kafka로 처리량 조절하기 데이터 정합성은 Redis로 이미 확인했으므로, 쿠폰 생성만 처리 Kafka 분산 이벤트 스트리밍 플랫폼 이벤트 스트리밍: 소스에서 목적지까지 이벤트를 실시간으로 스트리밍하는 것 Producer(소스) - Topic(큐) - Consumer(목적지) 장점 API에서 직접 생성하는 것에 비해 처리량 조절이 가능 -> DB 부하 감소 큐를 사용하므로 이벤트가 하나가 끝난 후 다음 이벤트가 처리되어 DB에 한 번에 쏠리지 않음 단점 이벤트 생산과 이벤트 처리는 약간의 텀이 발생 Producer의 이벤트 생산은 매우 빠르지만, Consumer는 이벤트를 처리하느라 시간차 발생 부록: 쿠폰 1인당 1개로 제한하기 DB 레벨 제한: Unique key 사용하기 userId, couponType에 유니크 제약 조건 걸기 문제점: 보통 서비스는 한 유저가 같은 타입의 쿠폰을 여러개 가질 수 있으므로, 실용적이지 않음 락 범위 넓혀서 쿠폰 발급 여부 조회해 판단하는 로직 추가하기 쿠폰 발급 여부 판단 로직 쿠폰 발급 여부 조회: select * from coupon where userId = ? 쿠폰이 이미 있다면 발급하지않고, 미지급일 때만 발급 락 범위: 쿠폰 발급 여부 조회 ~ Redis 동시성 체크 ~ 카프카 이벤트 생산 문제점 생산자와 소비자의 시간차 때문에 쿠폰이 여러 개 발급될 수 있음 소비자에서 아직 발급 중인데 유저의 쿠폰 발급 요청이 한 번 더 온다면? API에서 쿠폰 생성까지 하더라도 락 범위가 너무 넓어 성능 저하 발생 Set 자료구조 사용하기 (권장) userId를 Set에 저장하면 쿠폰 발급 여부를 바로 알 수 있음 Redis도 Set을 지원하므로 활용 부록: Consumer 예외 처리하기 문제: Consumer에서 예외가 발생하면, 발급된 쿠폰 개수는 올라갔는데 쿠폰은 발급되지 않은 상황 발생 즉, 100개보다 적은 쿠폰이 발생하는 상황 발생 가능 해결책: Consumer에서 예외 발생 시, 백업 데이터(FailedEvent 테이블)와 로그 남기기 추후 배치 프로그램으로 주기적으로 실패한 이벤트를 다시 처리해 쿠폰 발급
General
· 2025-01-06
동시성 문제 해결 방법
동시성 문제 해결방법 멀티스레드 작업을 하다보면, 공유 자원에 대한 Race Condition으로 인해 동시성 이슈가 발생한다 이에 대한 다양한 해결방법을 정리해보자 최종 선택 기준 synchronized는 사용 X 비용적 여유가 없고 적정한 트래픽이라면, MySQL로 처리 비용적 여유가 있고 트래픽이 많다면 Redis로 처리 MySQL VS Redis MySQL 이미 사용 중이라면 별도 비용 없이 사용 가능 Redis 보다 성능이 떨어짐 (어느정도 트래픽까지는 문제 없이 사용 가능) Redis 사용 중인 Redis가 없다면, 인프라 구축 및 관리 비용 발생 MySQL보다 성능이 좋음 Synchronized (거의 사용 X) 데이터에 1개의 스레드만 접근 가능하도록 하기 문제점 여러 프로세스 동작 시, 여전히 Race Condition 발생 synchronized는 하나의 프로세스 안에서만 1개의 스레드 접근 보장 다른 프로세스의 스레드가 접근하면, 여전히 여러 스레드가 접근 가능해짐 서버가 1대일 때는 괜찮지만, 2~3대부터는 데이터 접근을 여러 곳에서 할 수 있음 실제 운영 중인 서비스는 대부분 2대 이상의 서버를 사용 -> synchronized는 거의 사용 X 추가로, @Transactional 사용 시 synchronized 적용이 어려움 @Transactional은 스프링 AOP 사용으로 트랜잭션 프록시 객체를 생성 내부 동작 startTransaction(); stockService.decrease(id, quantity); (target 객체 호출) endTransaction(); 실제 DB 업데이트(endTransaction()) 전에 다른 스레드가 decrease() 메서드 호출할 수 있음 이렇게 되면, 다른 스레드는 갱신되기 전 값을 가져가 여전히 동시성 문제 발생 즉, 서비스 객체 메서드가 아닌, AOP 객체 메서드에 synchronized를 걸어야 하는데 어려움 MySQL이 지원해주는 방법 선택 기준 충돌이 빈번하게 일어날 것 같다면 Pessimistic Lock 충돌이 별로 없을 것 같다면 Optimistic Lock Pessimistic Lock 실제 데이터에 락을 걸어서 정합성을 맞추는 방법 (for update) Exclusive Lock을 걸게되며, 다른 트랜잭션에서는 락이 해제되기전에 데이터를 가져갈 수 없음 e.g. 서버 1이 락을 가져가면, 다른 서버(2, 3, 4…)는 락 획득 대기 데드락을 주의해야 함 장점 충돌이 빈번하게 발생한다면, Optimistic Lock 보다 성능이 좋을 수 있음 락 덕분에 데이터 정합성 보장 단점 락 자체로 인한 성능 감소 발생 Optimistic Lock 버전을 이용해 정합성을 맞추는 방법 데이터를 읽은 후 update 쿼리를 수행할 때, 현재도 내가 읽은 버전이 맞는지 확인하며 업데이트 e.g. 서버 1과 2가 버전 1인 데이터를 읽고, 업데이트 쿼리를 날림 서버 1이 업데이트 쿼리를 수행하면 해당 데이터의 버전이 2가 됨 서버 2는 읽은 버전이 1이므로, 현재 데이터와 버전(버전 2)이 달라 쿼리 실패 내가 읽은 버전에서 수정사항이 생겼을 경우, application에서 다시 읽은 후에 작업을 수행 장점 별도의 락을 걸지 않으므로Pessimistic Lock 보다 성능이 좋음 단점 업데이트가 실패했을 때 재시도 로직을 개발자가 직접 작성해야 함 Named Lock 이름을 가진 Metadata Locking 이름을 가진 락을 획득한 후 해제할 때까지 다른 세션은 이 락을 획득할 수 없음 MySQL: select get_lock('1', 1000), select release_lock(‘1’) 락 해제는 별도의 명령어로 수행하거나 선점시간이 끝나야됨 트랙잭션이 종료될 때 락이 자동으로 해제되지 않음 장점 분산 락 구현에 적합 Pessimistic Lock 보다 타임아웃을 쉽게 구현할 수 있음 삽입 시 데이터 정합성 맞출 때도 좋음 단점 트랜잭션 종료 시 락 해제, 세션 관리를 잘해주어야 함 실제 사용 시 구현 방법이 복잡 참고: 실무에서는 데이터 소스를 분리해서 사용할 것 (커넥션 풀 고갈 예방) Pessimistic Lock과의 차이점 Pessimistic Lock은 행, 테이블 단위로 락을 걸음 (e.g. Stock에 락을 걸음) Named Lock은 메타 데이터에 락을 걸음 (e.g. Stock이 아닌 별도의 공간에 락을 걸음) Redis를 이용한 방법 선택 기준 재시도가 필요하지 않은 락은 Lettuce 활용 재시도가 필요한 락은 Redisson 활용 Lettuce setnx 명령어(set if not exist)를 활용해 분산 락 구현 데이터 set (=락 획득), 데이터 del (=락 해제) MySQL의 Named Lock과 유사 스핀락(Spin Lock) 방식 장점 구현이 간단 세션 관리도 신경쓸 필요 X 별도 라이브러리 필요 X spring-data-redis를 사용하면 Lettuce가 기본 단점 재시도 로직을 개발자가 직접 작성 스핀 락이므로 충돌이 잦으면 Redis 부하 상승 -> 실패 시 재시도 시간에 텀을 두어 보완 (Thread.sleep(100)) Redisson Pub-Sub 기반으로 분산 락 제공 채널을 하나 만들어 락 점유 중인 스레드가 해제를 알리면 락 획득 대기 스레드는 락 획득 시도 장점 락 획득 재시도를 기본으로 제공 Pub-Sub 기반이므로 Lettuce에 비해 Redis 부하 적음 락 해제 후 알림으로 락 획득 시도는 1번 혹은 몇 번 정도만 진행함 단점 구현이 조금 복잡함 별도의 라이브러리 필요 O
General
· 2025-01-05
<
>
Touch background to close