-
동시성 제어 [2] DB Lock개인 공부/spring 2024. 8. 5. 19:53728x90반응형
🔴 분산 Lock
지난 번 글에 이어 동시성 문제를 해결해보려고 한다.
java 동시성 제어 [1]java 처리분산된 여러 서버에서 서버마다 락을 처리해놔도 여러 서버가 존재한다면 동시성은 해결될 수 없다. 그래서 여러 서버가 DB 한곳으로 요청할때 DB에서 락을 걸어 동시성 이슈를 해결하는 방법에 대해 알아보자.
🟠 DB Lock (비관적 락)
데이터를 저장하고 있다면 당연히 DB를 사용하고 있을것이다. RDB를 사용하고 있는 경우 query를 통해 Lock을 걸어줄 수 있다.
여기서 비관적 락이란 트랜잭션이 발생하면 DB에서 락을 걸어서 대기시키는 것이다.
🟢 Service
@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class DbLockService { private final MemberRepository memberRepository; @Transactional public void post(RequestDto requestDto) { Optional<MemberEntity> findMember = memberRepository.findByMemberIdForLock(); // 선착순 30명 if (findMember.isPresent()) { return ; } memberRepository.save(requestDto.toModel()); } }
이전에 소스와는 좀 다르다. 30번째가 되었을때 DB에 lock을 걸어주기 위해 쿼리를 수정해야되기 때문이다.
아래가 수정된 쿼리다.
public interface MemberRepository extends JpaRepository<MemberEntity, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(value = "select m from MemberEntity m where m.memberId >= 30") Optional<MemberEntity> findByMemberIdForLock(); }
실행되는 쿼리는
Hibernate: select me1_0.member_id,me1_0.number from member me1_0 where me1_0.member_id>=30 for update
for update
가 추가되어 member_id 값이 30이상인 경우 db lock이 걸려 insert시 대기하게 된다.👏 참고
JPA의 LockModeType 에서 사용되는 락의 종류에는 대표적으로
LockModeType.NONE: 어떠한 락도 설정하지 않습니다. 즉, 해당 엔티티에 대한 락을 설정하지 않습니다. 이는 기본적으로 JPA가 사용하는 모드이며, 다른 트랜잭션과 충돌 없이 엔티티를 읽거나 수정할 수 있습니다.
LockModeType.OPTIMISTIC: 낙관적 락을 설정합니다. 이 락 모드에서는 엔티티를 읽을 때만 락을 설정하고, 수정 시에는 락을 설정하지 않습니다. 따라서 다수의 트랜잭션이 동시에 엔티티를 읽을 수 있으며, 업데이트 시에만 충돌을 감지하여 처리합니다.
LockModeType.PESSIMISTIC_READ: 비관적 읽기 락을 설정합니다. 이 락 모드에서는 읽기 작업을 수행할 때 다른 트랜잭션이 해당 레코드를 수정하지 못하도록 읽기 락을 설정합니다. 따라서 읽기 작업을 수행하는 트랜잭션이 완료될 때까지 다른 트랜잭션이 해당 레코드를 수정할 수 없습니다.
LockModeType.PESSIMISTIC_WRITE: 비관적 쓰기 락을 설정합니다. 이 락 모드에서는 읽기 작업과 쓰기 작업을 수행할 때 해당 레코드에 대한 쓰기 락을 설정합니다. 따라서 해당 레코드에 대한 수정이 완료될 때까지 다른 트랜잭션이 해당 레코드에 대한 읽기 및 쓰기 작업을 수행할 수 없습니다.
Lock 종류가 존재한다. 각 상황에 맞게 사용하면 된다.
🟢 Test
@SpringBootTest class DbLockServiceTest { @Autowired private DbLockService dbLockService; // 동시성 제어 테스트 코드 @Test public void testConcurrentReservation() throws InterruptedException { int totalThreads = 100; int perThread = 1; CountDownLatch latch = new CountDownLatch(totalThreads); ExecutorService executorService = Executors.newFixedThreadPool(totalThreads); for (int i = 0; i < totalThreads; i++) { final int finalI = i; executorService.submit(() -> { try { for (int j = 0; j < perThread; j++) { RequestDto request = RequestDto.builder() .number(finalI) .build(); dbLockService.post(request); } } finally { latch.countDown(); } }); } latch.await(); executorService.shutdown(); } }
🟢 결과
🟠 DB Lock (낙관적 락)
DB에서 동시성 이슈가 발생했다고 판단되는 경우에 락을 걸어준다.
🟢 Service
🔵 entity
@Table(name = "O_MEMBER") @AllArgsConstructor @NoArgsConstructor @Builder @Getter @Setter @Entity public class OptimisticMemberEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; private int number; @Version private int version; }
동시성 이슈 처리를 위해 version 데이터를 가지고 있을 OptimisticMemberEntity 하나를 추가해준다.
🔵 repository
public interface OptimisticMemberRepository extends JpaRepository<OptimisticMemberEntity, Long> { }
🔵 service
@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class DbOptimisticLockService { private final OptimisticMemberRepository optimisticMemberRepository; private final MemberRepository memberRepository; @Transactional public void post(RequestDto requestDto) { Optional<OptimisticMemberEntity> findOptimistic = optimisticMemberRepository.findById(1L); if (findOptimistic.isEmpty()) { throw new RuntimeException("no data"); } OptimisticMemberEntity optimisticMemberEntity = findOptimistic.get(); int version = optimisticMemberEntity.getVersion(); if (version > 30) { throw new RuntimeException("30명 초과"); } optimisticMemberEntity.setNumber(version); optimisticMemberEntity.setVersion(++version); optimisticMemberRepository.save(optimisticMemberEntity); memberRepository.save(requestDto.toModel()); } }
🟢 Test
@SpringBootTest class DbOptimisticLockServiceTest { @Autowired private DbOptimisticLockService dbOptimisticLockService; @Autowired private OptimisticMemberRepository optimisticMemberRepository; // 동시성 제어 테스트 코드 @Test public void testConcurrentReservation() throws InterruptedException { optimisticMemberRepository.save(OptimisticMemberEntity.builder().number(1).version(1).build()); int totalThreads = 1000; int perThread = 1; CountDownLatch latch = new CountDownLatch(totalThreads); ExecutorService executorService = Executors.newFixedThreadPool(totalThreads); for (int i = 0; i < totalThreads; i++) { final int finalI = i; executorService.submit(() -> { try { for (int j = 0; j < perThread; j++) { RequestDto request = RequestDto.builder() .number(finalI) .build(); dbOptimisticLockService.post(request); } } finally { latch.countDown(); } }); } latch.await(); executorService.shutdown(); } }
테스트 코드에서 OptimisticMemberEntity 값을 미리 1개 세팅해준다. 이와 같은 예시로는 쿠폰 발급 이벤트를 다음과 같이 처리해줄 수 있을 것이다.
🟢 결과
✅ 고려해야할 점
DB락의 경우 잘못 설계했을 때 데드락에 빠질 수도 있고 DB 자체가 빠른 시스템은 아니기 때문에 성능면에서도 이득을 볼수 없다.
하지만 분산 시스템을 사용하면서 Redis나 다른 DB 시스템이나 또 다른 방법을 사용하여 동시성을 처리하지 않고 DB만 가지고도 동시성을 처리할 수 있으니 러닝 커브면에서는 매우 낮다고 할수 있다.728x90반응형'개인 공부 > spring' 카테고리의 다른 글
Spring Stop Watch 로직 속도 체크 (0) 2024.08.05 동시성 제어 [3] Redis (0) 2024.08.05 동시성 제어 [1] java 처리 (0) 2024.08.05 spring[19] Spring의 요청과 응답 과정 (0) 2021.06.19 spring[18] Response 응답 (0) 2021.06.19