동시성제어

yesrin·2023년 10월 24일

데이터베이스

목록 보기
2/5

동시성을 제어해줄 방법을 찾아보자!

1. 시스템에서 제어

Synchronized : 해당 메서드에 하나의 쓰레드만 접근이 가능하게 됨.

⇒ 서버가 한대일때 동시성 제어 가능.

2. 디비LOCK

  1. Pessimiatic Lock(비관적 락) : 실제로 데이터에 락을 거는 방식.

  1. Optimsistic Lock(낙관적 락) : 실제로 Lock을 이용하지 않고 버전을 이용해서 정확성을 맞추는방법. 내가 읽은 버전이 맞는지 확인 후 적용하는 방법. 버전이 일치하지 않으면 업데이트 실패. 실패할 경우 다시 시도하는 로직 필요

⇒ 탈락 : 충돌이 많이 발생하지 않는 환경에 적합.

@Version
private Long version;
@Lock(value = LockModeType.OPTIMISTIC)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithOptimisticLock(Long id);

업데이트 실패했을때 재시도 해야하므로 while 문으로 감싸줌

수량 감소에 실패한다면 50초 뒤에 재시도.

@Service
public class OptimisticLockStockFacade {

    private 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);
            }
        }
    }
}
  1. NamedLock : 이름을 가진 메타데이터 Lock , 락을 획득하고 해제 할때까지 다른 세션을 이를 획득할 수 없음.

    트렌잭션이 종료될때 락이 자동으로 해제되지 않기때문에 별도의 명령어로 해제를 실행해주거나 선점시간이 끝나야 락이 해제됨.

세션 1이 1이라는 이름으로 락을 획득

세션 2는 세션1이 락을 해재한후 락을 획득할 수 있음.

    @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);
public void purchaseItem(Long itemId, PurchaseRequestDto requestDto) {
   		try {
   			//NameLock 획득
   			lockItemRepository.getLock(itemId.toString());
  
  User user = userService.findUser(requestDto.getEmail());
   			Item item = itemService.findItem(itemId);
   			checkStock(item);
   			Purchase purchase = Purchase.builder().item(item).user(user).build();
   			purchaseRepository.save(purchase);
   			item.sellOne();
   		} finally {
   			//NamedLock 해제
   			lockItemRepository.releaseLock(itemId.toString());
   		}
   	}

트렌젝션 종료시에 락해제 세션관리를 잘 해줘야 하기때문에 주의해서 사용해야하고 실제로 사용할때는 구현방법이 복잡 할 수 있음.

실제로 우리 코드에서 테스트 해봤을때 동시성 제어가 잘 되지 않았음.ㅜㅜ

3. Redis

  1. Lettuce : spinLock 을 사용하므로 재시도 로직 짜주어야함.

    setnx 사용.

    namedLock과 거의 비슷. 다른점: 레디스 사용+세션관리에 신경안써도 됨

    *spinLock : 락을 획득하려는 쓰레드가 락을 획득할 수 있는지 반복적으로 확인하는 방법

일정시간 이후에 락 획득을 재시도 하는 로직 필요.
  1. reddion

    채널을 하나 만들고 락을 점유중인 쓰레드가 락 획득하려는 쓰레드에게 해제를 알려주면 안내를 받은 쓰레드가 락획득을 시도함.

    별도의 재시도 로직 필요하지 않음.

@Transactional
	public boolean purchaseItem(Long itemId, PurchaseRequestDto requestDto) {
		User user = userService.findUser(requestDto.getEmail());
		Item item = itemService.findItem(itemId);

		String lockKey = "item_lock_" + itemId;
		RLock rLock = redissonClient.getLock(lockKey);

		try {
			boolean available = rLock.tryLock(5, 3, TimeUnit.SECONDS);
			if (!available) {
				return false;
			}
			checkStock(item);
			Purchase purchase = Purchase.builder().item(item).user(user).build();
			purchaseRepository.save(purchase);
			item.sellOne();

		} catch (InterruptedException e) {
			e.printStackTrace();

		} finally {
			rLock.unlock(); // (4)
		}

		return false;
	}

출처
https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard

profile
안녕하세요! 틀린 정보는 댓글 달아 주세요.

0개의 댓글