1. 재고시스템
1). 문제점
- 레이스 컨디션: 둘 이상의 쓰레드가 공유 데이터에 액세스할 수 있고 동시에 변경을 하려고 할때 발생하는 문제
- 하나의 쓰레드가 작업이 끝나고 DB에 반영하기 전에 다른 쓰레드가 작업을 해서 생기는 문제
@Test
void 동시에_100개의_요청() throws Exception {
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();
Assertions.assertThat(stock.getQuantity()).isEqualTo(0L);
}

2. 자바 해결법
1). Synchronized
- @Transactional이 있으면 동작방식때문에 실패하게 된다
- @Transactional은 우리가 만든 클래스를 매핑한 클래스로 새로 만들어서 실행 (콜백패턴 이용)
- 서비스계층의 @Transactional 없애면 동시성 해결가능하다
startTransaction();
stockService.decrease(id, quantity);
endTransaction();
(1). 문제점
- 프로세스 안에서만 보장이 되기때문에 서버가 한 대일경우에는 문제가 없지만
- 서버가 두 대이상이면 데이터 접근이 여러곳에서 가능하기 때문에 다시 문제가 생긴다
3. MySql 해결법
- Pessimistic Lock: 실제로 락을 걸어서 정합성을 맞추는 방법, 데드락 주의
- Optimistic Lock: 실제 락은 아니고 버전을 확인해서 정확성을 맞추는 방법
- Named Lock: 이름을 가진 락을 획득, 별도의 명령어로 해제를 하거나 선점시간이 끝나야 해제
-> Perssimistic은 row나 테이블단위로 걸지만 Named는 메타데이터 락킹이다
1). Pessimistic Lock
- 충돌이 빈번하다면 Optimistic보다 성능면에서 낫다
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(Long id);
}
@Service
public class PessimisticLockStockService {
private final StockRepository stockRepository;
public PessimisticLockStockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findByIdWithPessimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
}
2). Optimistic Lock
- 보통은 Pessimistic Lock보다 성능면에서 낫다
- 재시도 로직을 개발자가 직접 짜야됨
@Version
private Long version;
@Lock(LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
@Service
public class OptimisticLockStockService {
private final StockRepository stockRepository;
public OptimisticLockStockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findByIdWithOptimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
}
@Component
public class OptimisticLockStockFacade {
private final OptimisticLockStockService optimisticLockStockService;
public OptimisticLockStockFacade(OptimisticLockStockService optimisticLockStockService) {
this.optimisticLockStockService = optimisticLockStockService;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (true) {
try {
optimisticLockStockService.decrease(id, quantity);
break;
} catch (Exception e) {
Thread.sleep(50);
}
}
}
}
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
try {
optimisticLockStockFacade.decrease(1L, 1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
Assertions.assertThat(stock.getQuantity()).isEqualTo(0L);
3). NamedLock
- 락이 자동으로 해제되지 않기때문에 해제를 명시적으로 해줘야한다
- get-lock과 release-lock으로 획득, 해제가 가능하다
- pessimistck이 stock에 직접적으로 락을 걸었다면 named는 별도의 공간에 락을 건다
- 주로 분산락을 구현할때 사용

public interface LockRepository extends JpaRepository<Stock, Long> {
@Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
void getLock(String key);
@Query(value = "select release_lock(:key)", nativeQuery = true)
void releaseLock(String key);
}
@Component
public class NamedLockStockFacade {
private final LockRepository lockRepository;
private final StockService stockService;
public NamedLockStockFacade(LockRepository lockRepository, StockService stockService) {
this.lockRepository = lockRepository;
this.stockService = stockService;
}
@Transactional
public void decrease(Long id, Long quantity) {
try{
lockRepository.getLock(id.toString());
stockService.decrease(id, quantity);
} finally {
lockRepository.releaseLock(id.toString());
}
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id, Long quantity) {
Stock stock = stockRepository.findById(id).orElseThrow();
stock.decrease(quantity);
stockRepository.saveAndFlush(stock);
}
4. Redis 해결법
- 분산락을 구현하는 대표적인 2가지
- Lettuce: setnx 명령어를 활용한 분산락 방식, spin lock 방식
- Redisson: pub-sub기반으로 lock 구현 제공
1). Lettuce
- spin lock 방식이기때문에 레디스에 부하를 줄수 있다. Thread.sleep으로 텀을 줘야한다
@Component
public class RedisLockRepository {
private RedisTemplate<String, String> redisTemplate;
public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public Boolean lock(Long key) {
return redisTemplate
.opsForValue()
.setIfAbsent(generateKey(key),"lock", Duration.ofMillis(3_000));
}
public Boolean unlock(Long key) {
return redisTemplate.delete(generateKey(key));
}
private String generateKey(Long key) {
return key.toString();
}
}
@Component
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private StockService stockService;
public LettuceLockStockFacade(RedisLockRepository repository, StockService stockService, RedisLockRepository redisLockRepository) {
this.stockService = stockService;
this.redisLockRepository = redisLockRepository;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(id)) {
Thread.sleep(100);
}
try{
stockService.decrease(id, quantity);
}finally {
redisLockRepository.unlock(id);
}
}
}
2). Redisson
- 락획득을 한번 또는 적게만 시도하므로 레디스의 부하를 줄여주게 된다
- 락 획득 재시도가 기본 제공된다.
- 의존성 추가:
implementation group: 'org.redisson', name: 'redisson-spring-boot-starter', version: '3.31.0'
@Component
public class RedissonLockStockFacade {
private RedissonClient redissonClient;
private StockService stockService;
public RedissonLockStockFacade(RedissonClient redissonClient, StockService stockService) {
this.redissonClient = redissonClient;
this.stockService = stockService;
}
public void decrease(Long id, Long quantity) {
RLock lock = redissonClient.getLock(id.toString());
try{
boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);
if(!available){
System.out.println("Lock 획득 실패");
return;
}
stockService.decrease(id,quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}