동시성 문제 해결방법
- 멀티스레드 작업을 하다보면, 공유 자원에 대한 Race Condition으로 인해 동시성 이슈가 발생한다
- 이에 대한 다양한 해결방법을 정리해보자
-
최종 선택 기준
-
synchronized
는 사용 X - 비용적 여유가 없고 적정한 트래픽이라면, MySQL로 처리
- 비용적 여유가 있고 트래픽이 많다면 Redis로 처리
-
- MySQL VS Redis
- MySQL
- 이미 사용 중이라면 별도 비용 없이 사용 가능
- Redis 보다 성능이 떨어짐 (어느정도 트래픽까지는 문제 없이 사용 가능)
- Redis
- 사용 중인 Redis가 없다면, 인프라 구축 및 관리 비용 발생
- MySQL보다 성능이 좋음
- 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
를 걸어야 하는데 어려움
-
-
여러 프로세스 동작 시, 여전히 Race Condition 발생
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)이 달라 쿼리 실패
- e.g.
- 내가 읽은 버전에서 수정사항이 생겼을 경우, application에서 다시 읽은 후에 작업을 수행
- 장점
- 별도의 락을 걸지 않으므로Pessimistic Lock 보다 성능이 좋음
- 단점
- 업데이트가 실패했을 때 재시도 로직을 개발자가 직접 작성해야 함
- Named Lock
- 이름을 가진 Metadata Locking
- 이름을 가진 락을 획득한 후 해제할 때까지 다른 세션은 이 락을 획득할 수 없음
- MySQL:
select get_lock('1', 1000)
, select release_lock(‘1’)
- MySQL:
-
락 해제는 별도의 명령어로 수행하거나 선점시간이 끝나야됨
- 트랙잭션이 종료될 때 락이 자동으로 해제되지 않음
- 장점
- 분산 락 구현에 적합
- 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
-
Pub-Sub 기반으로 분산 락 제공