ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동시성 제어 [2] DB Lock
    개인 공부/spring 2024. 8. 5. 19:53
    728x90
    반응형

    🔴 분산 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 에서 사용되는 락의 종류에는 대표적으로

    1. LockModeType.NONE: 어떠한 락도 설정하지 않습니다. 즉, 해당 엔티티에 대한 락을 설정하지 않습니다. 이는 기본적으로 JPA가 사용하는 모드이며, 다른 트랜잭션과 충돌 없이 엔티티를 읽거나 수정할 수 있습니다.

    2. LockModeType.OPTIMISTIC: 낙관적 락을 설정합니다. 이 락 모드에서는 엔티티를 읽을 때만 락을 설정하고, 수정 시에는 락을 설정하지 않습니다. 따라서 다수의 트랜잭션이 동시에 엔티티를 읽을 수 있으며, 업데이트 시에만 충돌을 감지하여 처리합니다.

    3. LockModeType.PESSIMISTIC_READ: 비관적 읽기 락을 설정합니다. 이 락 모드에서는 읽기 작업을 수행할 때 다른 트랜잭션이 해당 레코드를 수정하지 못하도록 읽기 락을 설정합니다. 따라서 읽기 작업을 수행하는 트랜잭션이 완료될 때까지 다른 트랜잭션이 해당 레코드를 수정할 수 없습니다.

    4. 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
Designed by Juno.