코딩

[DB] 동시성에 관한 고찰 본문

DB

[DB] 동시성에 관한 고찰

ssooyn_n 2022. 12. 12. 16:53

crew를 만들기 위해 신청서를 보낼 때,

최대인원이 5명이고 현재원이 1명인 방에 여러명을 동시에 승낙받았을 경우, 4명만 들어와야하는데 요청한 7명이 다 들어왔다.

동시성을 해결하기 위한 방법에는 여러가지가 있지만 이 경우 트래픽의 수가 낮고 대용량 데이터가 오고가지 않으므로

데이터베이스에서 동시성을 해결해보도록 했다.

 

crewID = 8

해결

  • 비관적 Lock Write 옵션을 사용하여 해당 크루 데이터에 Lock을 걸어 다른 트랜잭션이 해당 크루를 select하지 못하게 처리함.
  •  

테스트 코드

		
@Autowired
CrewService crewService;

		@Test
    @DisplayName("크루 참가 동시성 테스트 -> Service 직접 호출")
    void crewServiceTest() throws InterruptedException {
        AtomicInteger successCount = new AtomicInteger();
        int numberOfExcute = 7;
        ExecutorService service = Executors.newFixedThreadPool(7);
        CountDownLatch latch = new CountDownLatch(numberOfExcute);

        // when
        for (int i = 0; i < numberOfExcute; i++) {
            int finalI = i;
            service.execute(() -> {
                try {
                    crewService.accessJoinCrew(UUID.fromString("11ed5e5f-d431-a2da-a087-db4453b45882"), (50000L + finalI));
                    System.out.println("TestId : " + finalI);
                    successCount.getAndIncrement();
                } catch (ObjectOptimisticLockingFailureException oe) {
                    System.out.println("충돌감지");
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
                latch.countDown();
            });
        }
        latch.await();

        // then (총원 5, 현재원 1, 4번의 성공만 일어나야함.)
        assertThat(successCount.get()).isEqualTo(4);
    }

CrewService

// 크루 가입신청 허가하기
    @Override
    public void accessJoinCrew(UUID memberId, Long joinWaitingId) {
        Member crewKing = memberRepository.findById(memberId)
                .orElseThrow(() -> new NotFoundException(USER_NOT_FOUND));

        JoinWaiting findJoinWaiting = joinWaitingRepository.findByIdWithMemberAndCrew(joinWaitingId)
                .orElseThrow(() -> new NotFoundException(JOINWAITING_NOT_FOUND));

        // 허가하는 사람이 크루장인지 검사
        if (!findJoinWaiting.getCrew().getCrewMaster().getId().equals(crewKing.getId())) {
            throw new NotMatchException(CREW_KING_NOT_MATCH);
        }
        Long crewId = findJoinWaiting.getCrew().getId();

        // 영속성 컨텍스트 초기화
        em.clear();

        // 크루 최대 참여자 수 안넘는지 체크(여기서 비관적 Lock이 걸림->Row Lock)
        Crew joinCrew = crewRepository.findByCrewIdForLock(crewId).get();
        System.out.println("현재원 수:" + joinCrew.getMemberCrewList().size());
        if (joinCrew.getMemberCrewList().size() >= joinCrew.getMaxParticipantCnt()) {
            throw new NotMatchException(CREW_MAX_PARTICIPANT_CNT_NOT_MATCH);
        }

        // 가입 처리
        memberCrewRepository.save(MemberCrew.create(findJoinWaiting.getMember(), findJoinWaiting.getCrew()));
        // 대기목록에서 삭제
        joinWaitingRepository.delete(findJoinWaiting);
    }

CrewRepository

@Query("select c from Crew c join fetch c.memberCrewList where c.id = :crewId")
    @Lock(value = LockModeType.PESSIMISTIC_WRITE) // 비관적 락(Row Lock, WRITE 옵션이라 다른 트랜잭션에서 읽기, 쓰기 다 안됨)
    Optional<Crew> findByCrewIdForLock(Long crewId);
  • 참가신청 식별자로 join을 통해 crew를 가져옴
  • crew의 식별자를 따로 저장
  • 영속성 컨텍스트를 초기화(영속성 컨텍스트에 있는 crew는 Lock이 걸려있지 않은 데이터. 다른 트랜잭션에서 접근하면 바로 읽을 수 있음. 해당 크루 데이터로 현재원 수를 가져오기 때문에 이 크루에다가 Lock을 걸어줘야 함.) 하고 Row Lock을 거는 select for update문을 통해 Crew 데이터를 가져옴
  • 크루 현재원 수가 최대 인원 수보다 작으면 가입 처리.

💡 삽질

  • @Transactional은 aop통해 프록시 객체로 실행되기 때문에 빈으로 등록 안된 메소드로 실행하면 스프링이 관리해주는 트랜잭션으로 실행이 안됨
  • mysql은 mvcc로 동시성 관리를 해주는데 이거때문에 비관적락 write옵션을 걸어도 읽기가 가능해짐(그럼 어떻게 하라는거지)
    • select for update로 락이 걸림
    • 다른 트랜잭션에서 select를 하면 쿼리는 날라가긴 함
    • 근데 db에서 수행을 안하고 대기하고 있다가 커밋이 되면 수행해서 결과를 리턴해줌

💡 응용

  • 현재 로직
  • 프론트에서 웹소켓통신으로 현재응찰가격보다 높은 호가를 보내줌
  • 이것을 일단 DB에 다 저장
  • 그리고 요청이 들어온 시점에서 최고가로 응찰한 데이터를 프론트에 리턴
    • 최고가로 응찰한 데이터가 여러개일 경우 제일 먼저 들어온 응찰 데이터만 리턴
    • 즉, 제일 먼저 응찰 요청을 한 클라이언트가 응찰 성공자가 되는 것임
  • 하지만, 같은 시간에 동시에 응찰 요청이 들어온다면 응찰 성공자가 여러명이 되는 동시성 문제가 발생
  • 해결 방법
  • Table Lock 걸기 → 제일 간단한 방법 같지만 성능 문제가 최악일거같음

 

 

참고

https://catch-me-java.tistory.com/60

https://findmypiece.tistory.com/309

Comments