코딩
[DB] 동시성에 관한 고찰 본문
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 걸기 → 제일 간단한 방법 같지만 성능 문제가 최악일거같음
참고
'DB' 카테고리의 다른 글
[JPA] nativeQuery에서 dto 매핑하기 (0) | 2022.12.12 |
---|---|
[MySQL] Point Column 성능에 관한 고찰 (0) | 2022.12.12 |
공공데이터 API parser 데이터 오류잡기 (0) | 2022.05.13 |
Comments