동시성 이슈 해결하는 방법
문제해결
1. Application Level
synchronized 사용
2. Database
Database가 제공하는 lock을 사용
Pessimistic lock
Optimistic lock
'
named lock
Redis Distribute lock
분산환경에서 레디스를 활용하여 동시성 제어
문제점
공유 자원에 여러 스레드가 동시에 접근하는 race condition 상태일 때 동시성 이슈 발생 함.
@Test
public void 동시에_100개의_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for(int i=0; i<threadCount; i++){
executorService.submit(()->{
try {
// 아무 처리 안 된 상태에서 여러 쓰레드에서 동시에 재고 값 감소시키는 로직 수행
stockService.decrease(1l, 1l);
} finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - (1*100) = 0 기대
assertEquals(0,stock.getQuantity());
}
1. Application Level
synchronized 사용
메서드 선언부에 synchronized 키워드 붙이면, 해당 메서드에는 1개의 스레드만 접근 가능하게 된다.
@Transactional
synchronized public void decrease(Long id, Long quantity){
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
한계 : synchronized는 하나의 프로세스 안에서만 보장이 된다.
즉 서버가 1개인 경우는 데이터 접근을 하나의 서버에서만 접근해서 문제 없지만, 서버가 2대 그 이상일 경우 여러 서버에서 데이터에 접근하게 된다면 race condition 문제는 동일하게 발생하게 된다.
즉, 여러 프로세스에서 data에 접근하게 된다면 사용할 수 없다.
ThreadLocal 사용
threadlocal을 사용하면 일반 멤버변수와 달리 쓰레드별로 해당 전용 공간을 만들어서 저장을 하게 되어서 동시성 문제가 해결된다.
private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();
traceIdHolder.get();
traceIdHolder.set(new TraceId());
Threadlocal 사용시 주의사항 : 쓰레드로컬 저장소는 일반적으로 thread가 종료되면 같이 사라진다.
그래서 thread가 종료되지 않는 경우 문제가 발생할 여지가 있는데, was에서 thread pool 활용하는 경우는 thread가 종료되지 않아 thread local 저장소가 계속 살아 있어서, 잘못된 데이터 접근이 발생할 수 있다.
2. Database가 제공하는 lock 활용
Pessimistic lock - 특정 row를 사용할 때 lock을 걸고, 해당 트랜잭션이 끝나기 전까지 다른 트랜잭션이 특정 row의 lock을 얻는 것을 방지한다.
아래는 로깅된 sql 로그의 일부인데, sql 쿼리문의 for update 부분이 lock 거는 명령이다.
Hibernate: 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
Optimistic lock - lock을 거는 방식이 아니고, version 같은 칼럼을 만들어서 update 시 해당 row의 버전이 select 했을 때 버전과 다르다면 문제 발생시키는 방식으로 데이터 체크 함.
장점 : lock을 잡지 않으므로 Pessimistic lock 보다 성능상 좋음
단점 : update 실패 시 재시도 로직 개발자가 직접 수행하여야 함
충돌이 빈번하게 일어나는 경우 : Pessimistic lock
충돌이 간헐적으로 일어나는 경우 : Optimistic lock
named lock -
3. Redis를 사용하여 동시성 문제 해결
분산 락 구현하는 라이브러리
Lettuce
- setnx 명령어를 활용하여 분산락 구현 ( key/value 를 set 할 때 기존의 값이 없을 때만 set 하는 명령어)
- spin lock 방식 (lock을 획득하려는 thread 가 lock을 사용할 수 있는지 반복적으로 확인하면서 lock 획득을 시도하는 방식) - retry 로직을 개발자가 작성하여야 함
장점 : 구현이 간단하다
단점 : spin lock 방식
예시에서는 DB에서 읽어온 Entity의 ID 값을 Redis에 lock하여 동시성 제어하는 것으로 보임
Redisson
- pub-sub 기반으로 lock 구현 제공 ( 채널을 하나 만들고, lock을 점유중인 스레드가 lock 획득 대기중인 스레드에게 해제를 알려주면 락을 획득하는 방식이다.)
장점 : pub/sub 기반 구현이기에 redis의 부담이 줄어듬
단점 : Lettuce에 비해서 복잡하다는 단점, 별도 라이브러리 사용 필요하다는 점
댓글