동시성 제어 [2] DB Lock

최준호·2024년 5월 2일
1

동시성

목록 보기
2/3
post-thumbnail
post-custom-banner

🔴 분산 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만 가지고도 동시성을 처리할 수 있으니 러닝 커브면에서는 매우 낮다고 할수 있다.

깃허브 소스

profile
해당 주소로 이전하였습니다. 감사합니다. https://ililil9482.tistory.com
post-custom-banner

0개의 댓글