본문 바로가기
카테고리 없음

230419 TIL

by hbIncoding 2023. 4. 19.

1.  테스트 코드 작성하기

  • Service
	@Transactional
	public void bookorder(Long bookid, Integer quantity, Member member){

		Book book = entityManager.find(Book.class,bookid,LockModeType.PESSIMISTIC_FORCE_INCREMENT);

		Long inven = book.getInventory()-quantity;
		if (inven<0){
			throw new CustomException(ErrorCode.INVALIDATION_NOT_ENOUGH);
			return;
		}else {
			book.orderbook(inven);
			jumoonRepository.save(new Jumoon(member,book,quantity));
		}
	}
  • testcode
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class LockTest {

	@Autowired
	MemberRepository memberRepository;

	@Autowired
	BookRepository bookRepository;

	@Autowired
	JumoonRepository jumoonRepository;

	@Autowired
	JumoonService jumoonService;

	@Autowired
	EntityManager entityManager;

	@RepeatedTest(10)
	void testConnection() throws InterruptedException {

		Book book = new Book("큰카테","작은카테","제목스",9999,9
					,"작가스","출판스",null,1993,5,20L);
		bookRepository.save(book);

		List<Member> members = new ArrayList<>();
		for (int i = 0; i < 20; i++) {
			String email = "testman"+i+"@ntest.com";
			Member member = new Member(email);
			memberRepository.save(member);
			members.add(member);
		}

		CountDownLatch countDownLatch = new CountDownLatch(20);

		List<JumoonWorker> workers = Stream.
			generate(()-> new JumoonWorker(members.get(0),book,3,countDownLatch))
				.limit(20)
					.collect(Collectors.toList());

		workers.forEach(worker -> new Thread(worker).start());
		countDownLatch.await(1, TimeUnit.SECONDS);

		Book updatebook = bookRepository.findById(book.getId()).orElseThrow(
			()-> new CustomException(ErrorCode.BOOK_NOT_FOUND)
		);

		List<Jumoon> jumoons = jumoonRepository.findAllByBook_Id(updatebook.getId());
		System.out.println("jumoons.size() = " + jumoons.size());


		assertEquals(6,jumoons.size());
		assertEquals(2,updatebook.getInventory());

	}

	private class JumoonWorker implements Runnable{

		private Member member;
		private Book book;
		private int quantity;
		private CountDownLatch countDownLatch;

		public JumoonWorker(Member member, Book book, int quantity, CountDownLatch countDownLatch) {
			this.member = member;
			this.book = book;
			this.quantity = quantity;
			this.countDownLatch = countDownLatch;
		}

		@Override
		public void run() {
			jumoonService.bookorder(book.getId(), quantity,member);
			countDownLatch.countDown();
		}
	}
}
  • 코드설명
    • service에서 bookid를 이용해 book을 불러오면서 lock을 건다.
    • 20개의 쓰레드를 통해 동시성 문제를 발생시키는 테스트를 10번 진행한다.
    • 그리고 해당 책의 재고량과 총 주문 개수를 파악하여 동시성 제어가 잘 되었는지 파악한다.

2.  Lock 옵션에 따른 실험 결과

 1)Lock 옵션 종류와 설명

   ㄱ. Optimistic

  • NONE : 별도의 옵션을 사용하지 않아도 Entity에 @Version이 적용된 필드만 있으면 낙관적 잠금이 적용됩니다.
  • OPTIMISTIC(read) : Entity 수정시에만 발생하는 낙관적 잠금이 읽기 시에도 발생하도록 설정합니다.
    읽기시에도 버전을 체크하고 트랜잭션이 종료될 때까지 다른 트랜잭션에서 변경하지 않음을 보장합니다.
    • 이를 통해 diry read와 non-repeatable read를 방지
  • OPTIMISTIC_FORCE_INCREMENT(write) : 낙관적 잠금을 사용하면서 버전 정보를 강제로 증가시키는 옵션

  ㄴ. Pessimistic

  • PESSIMISTIC_READ : dirty read가 발생하지 않을 때마다 공유 잠금(Shared Lock)을 획득하고 데이터가 UPDATE, DELETE 되는 것을 방지 할 수 있습니다.
  • PESSIMISTIC_WRITE : 배타적 잠금(Exclusive Lock)을 획득하고 데이터를 다른 트랜잭션에서 READ, UPDATE, DELETE 하는것을 방지 할 수 있습니다.
  • PESSIMISTIC_FORCE_INCREMENT : 이 잠금은 PESSIMISTIC_WRITE와 유사하게 작동 하지만 @Version이 지정된 Entity와 협력하기 위해 도입되어 PESSIMISTIC_FORCE_INCREMENT 잠금을 획득할 시 버전이 업데이트 됩니다.

 2)실험 결과 요약

  • 현재 서비스에서는 PESSIMISTIC_WRITE만 제대로 작동한다.
  • Optimistic은 성능은 좋지만 주문이 폭주하는 경우 트랜잭션을 시작할 때 저장한 버전 값과 트랜잭션이 끝나고 난 후 체크한 버전값이 다른 경우가 많은데 이 때 잘 작동하지 않는다. 따라서 진행하는 서비스에는 적합하지 않다.
  • 또한 Force_Increment 옵션의 경우 지원하는 DB의 종류가 정해져있는데, 현재 사용중인 mysql에서는 사용이 불가능하다.
  • PESSIMISTIC_READ는 어떤 트랜잭션이 수정중일 때 다른 트랜잭션에서 읽기는 가능하다. 하지만 현재 실험에서는 모든 트랜잭션들이 수정하고자 하기 때문에 적합하지 않다.
  • PESSIMISTIC_WRITE를 사용한 결과

3.  Lock을 거는 방법

  • 주로 저장된 entity를 불러올 때 사용된다. 따라서 jpa단위에서 걸어주거나 entityManager를 이용해 걸어주는 방법이 있다. 
  • @Transaction과 함께 @Lock을 걸어줘도 된다고 알고있었으나 잘못된 정보를 알고 있던 거였다.

4.  Redisson Distribution Lock

  • Transaction이 Lock을 점거하고 있다는 정보를 redis 서버(캐시 서버)에 올려서, 분산된 서버에서 하나의 DB를 조회하고 수정할 때 사용하기에 적절한 방법이다.
  • 아래 자세히 설명하겠지만, 요청 Thread를 순서대로 처리한다는 보장이 없다는 단점이 있다.
  • 서비스 로직만 아래와 같이 바꿔준다.
    • redis 서버에 lockname을 key로 가진 데이터가 없으면 만들고 lock을 건다
    • 이미 lock이 있다면, 기다리거나 포기한다. 코드 블럭 아래에 정확히 설명하겠다.
    • 트랜잭션을 성공적으로 수행했다면 lock을 해제한다.
	private final RedissonClient redissonClient;
    
    @Transactional
	public void bookorderredis(Long bookid, Integer quantity, Member member){
		final String lockname = bookid + ":lock";
		final RLock lock = redissonClient.getLock(lockname);
		final String worker = Thread.currentThread().getName();

		try{
			if(!lock.tryLock(1,3,TimeUnit.SECONDS)){
				System.out.println("키 점거 실패");
				return;
			}

			Book book = entityManager.find(Book.class, bookid);

			Long inven = book.getInventory() - quantity;

			if (inven<0){
				System.out.println("재고 없는데용");
			}else {
				book.orderbook(inven);
				jumoonRepository.save(new Jumoon(member,book,quantity));
			}
		}catch (Exception e){
			e.printStackTrace();
		}finally {
			if (lock != null && lock.isLocked()){
				lock.unlock();
			}
		}

	}
  • 코드 설명
    • trylock(long waitTime, long leaseTime, TimeUnit unit)
      • waitTime : 락을 사용할 수 있을 때 까지 waitTime만큼 기다린다.
      • leaseTime : leaseTime이 지나면 자동으로 락이 해제된다.
      • timeunit : 시간의 단위를 설정해준다.
    • unlock : 가지고 있던 락을 해제한다.

 1)실험 결과 요약

  • 잘 작동하지만 아래와 같이 주문 id가 순서대로 되어있지 않다. 즉 요청된 Thread를 순서대로 처리하지 않는다.
    • 이러한 단점을 해결하기 위해선 Redisson Distribution Lock 이 아니라 Redisson Fair Lock을 사용해한다.
    • 하지만 이 경우에는 DeadLock 문제가 발생할 가능성이 있다.
  • 순서대로 처리하지 않는 이유는 먼저 Thread가 lock을 선점한 동안 여러개의 Thread가 대기중일 때, 먼저 선점한 Thread가 트랜잭션을 완료 한 이후 어떤 Thread가 lock을 점유할지 모르기 때문이다.
    • Thread1번이 3초가 걸리는 Transaction을 진행중이다.
    • 이때 Thread 2번 부터 10번까지 대기를 한다. 
    • 2~10번까지 Thread1 이 끝날때 까지 계속 lock 점거를 요청한다.
    • 2~10번이 계속 요청하기 때문에 1번 종료 시점에 누가 점유하게 될지 장담할 수 없다.

2.  Lock을 거는 방법

  • 주로 저장된 entity를 불러올 때 사용된다. 따라서 jpa단위에서 걸어주거나 entityManager를 이용해 걸어주는 방법이 있다. 
  • @Transaction과 함께 @Lock을 걸어줘도 된다고 알고있었으나 잘못된 정보를 알고 있던 거였다.