티스토리 뷰
재고시스템으로 알아보는 동시성 이슈 해결방법
해당 글은 인프런 | 재고시스템으로 알아보는 동시성 이슈 해결방법 강의를 보고 정리한 자료입니다.
모든 코드는 Github에 공개되어 있으며 각 해결 단계를 커밋으로 분리해두었으니 확인해주세요.
상품이 판매될 때마다 재고를 하나씩 줄여주는 기능을 만들어본다.
이 때 발생할 수 있는 동시성 문제를 하나씩 해결해보자.
문제발생 케이스
하나의 상품이 100개의 재고를 가지고 있을 때 단순하게 생각하면 판매될 때 재고를 -1 한 후 저장해주면 될거라고 생각할 수 있지만, 실무에서는 다양한 케이스가 발생할 수 있다.
대표적으로,
하나의 상품에 주문이 동시에 (ms 차이 수준) 들어온다고 가정을 한다.
이럴 때, Race Condition이 발생하여 최종 재고 값이 이상하게 나올 수 있다.
문제의 상황 테스트 코드
위 예시에서 100갸의 쓰레드가 같은 상품의 재고를 감소시켰을 때 최종 재고는 0을 기대하지만 실제 테스트는 실패하는 것을 확인 할 수 있다.
각 단계별로 해결을 하며 문제점을 살펴봅니다.
1. Syncronized (level 1)
자바-synchronized에-대하여
Java의 기본 Syncronized 키워드를 사용해서 여러 쓰레드가 동시에 해당 접근할 수 없도록 한다.
Syncronized를 활용한 개선 커밋
결과
예시처럼 코드를 작성하면 쓰레드 동기화를 통해 해결했다고 생각할 수 있지만, 결과는 실패한다.
이유는 @Transactiional
Proxy 방식 때문에 Proxy가 (Commit)종료되기 전에
다른 쓰레드가 해당 자원에 접근할 수 있기 때문이다.
@Transactional 동작원리
해결법
서비스에 붙어있는 @Transactional
을 없앤다..@Transactional
을 제거하면 Proxy로 동작하지 않기 떄문에 해결 할 수는 있다.
문제점
Syncronized는 하나의 프로세스에서만 동기화를 보장한다.
현대의 어플리케이션은 2대 이상의 서버를 사용하기 때문에 Syncronized 로는 보장할 수 없다.
2. Mysql 을 활용한 다양한 방법 (level 2)
Pessimistic Lock을 활용
- 실제 데이터에 Lock을 걸어서 정합성을 맞추는 방법
- Exclusive Lock을 걸게되면 다른 트랜잭션에서는 Lock이 해제되기 전에는 데이터를 가져갈 수 없음
- Dead Lock 가능성이 있기 때문에 주의필요
select stock0_.id as id1_0_, stock0_.product_id as product_2_0_, stock0_.quantity as quantity3_0_ from stock stock0_ where stock0_.id=? for update
select..for update
를 사용하여 해당 row를 다른 세션에서 건들 수 없도록 Lock을 건다.
장점
- 충돌이 빈번하게 일어나면 Optimistic Lock보다 성능이 좋을 수 있음
- Lock을 잡기 때문에 데이터 정합성을 지킬 수 있음
단점
- 성능감소
Optimistic Lock
- 실제 Lock을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법
- 먼저 데이터를 읽은 후 update를 수행할 때 현재 내가 읽은 버전이 맞는지 확인하여 업데이트
- 내가 읽은 버전에서 수정사항이 생겼을 경우 application 단에서 다시 읽은 후 작업을 수행해야 함
장점
- 별도의 Lock이 없기 때문에 성능상 이점
단점
- 실패했을 시 로직을 개발자가 직접 만들어야함
- 충돌이 빈번히 일어나면 성능상 손해
Named Lock
- 이름을 가진 Metadata Locking (Table/Row Lock 이 아님)
- 이름을 가진 Lock을 획득한 후 해제할 때 까지 다른 세션은 이 Lock 을 획득할 수 없도록 합니다.
- Transaction이 종료될 때 자동으로 해제되지 않음. 별도의 명령어로 해제/선점 시간이 끝나야 해제됨
JPA Native Query를 이용
Connction Pool 이 부족해질 수 있기 때문에 별도의 Datasource를 이용하기를 추천
참고 자료
exclusive_lock
innodb-locking
locking-functions
Redis 활용 (level 3)
1. Lettuce
setnx
명령어 사용- spin lock 방식
- retry 로직 필요
장점
- 구현이 간단하다
- spring data redis 를 이용하면 lettuce 가 기본이기때문에 별도의 라이브러리를 사용하지 않아도 된다.
단점
- spin lock 방식이기때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis 에 부하가 갈 수 있다.
- Thread.sleep으로 부하를 줄여줬음
2. Redisson
- Pub-Sub 기반의 Lock 구현 제공
- 채널을 만들고 Lock을 획득한 Thread가 Lock획득을 시도하는 Thread에게 해제되었음을 알려주는 방식
장점
- 락 획득 재시도를 기본으로 제공한다.
- pub-sub 방식으로 구현이 되어있기 때문에 lettuce 와 비교했을 때 redis 에 부하가 덜 간다.
단점
- 별도의 라이브러리를 사용해야한다.
- lock 을 라이브러리 차원에서 제공해주기 떄문에 사용법을 공부해야 한다.
실무에서는 ?
- 실패 시 재시도가 필요하지 않은 lock 은 lettuce 활용
- 실패 시 재시도가 필요한 경우에는 redisson 를 활용
Mysql vs Redis
Mysql
- 이미 Mysql 을 사용하고 있다면 별도의 비용없이 사용가능하다.
- 어느정도의 트래픽까지는 문제없이 활용이 가능하다
- Redis 보다는 성능이 좋지않다.
Redis
- Mysql 보다 성능이 좋다.
- 활용중인 Redis 가 없다면 별도의 구축비용과 인프라 관리비용이 발생한다.
https://velog.io/@hgs-study/redisson-distributed-lock
https://techblog.woowahan.com/2631/
https://kwonnam.pe.kr/wiki/database/mysql/user_lock
'Dev > Spring' 카테고리의 다른 글
[security] 4. PasswordEncoder 설정하기 (0) | 2021.05.11 |
---|---|
[security] 3. JPA와 Security 연동 (0) | 2021.05.11 |
[security] 2. Inmemory User (0) | 2021.05.11 |
[security] 1. 기본 설정 (0) | 2021.05.11 |
Spring @EventListner 사용해보기 - 2 (0) | 2020.11.22 |
- Total
- Today
- Yesterday
- 개발
- 스프링부트
- Security
- springsecurity
- paawordencoder
- QueryDSL
- spring web
- 스프링
- ubuntu
- 웹서비스
- 유사결과
- java11
- springboot
- JPA
- howtoinstallnginx
- @formula
- Spring
- API
- 스프링시큐리티
- ControllerAdvice
- Java
- like절
- 스프링부트 시작하기
- boot
- ResourceHttpReqeustHandler
- FastAPI
- booleanExpression
- 자바
- mapstruct
- Python
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |