jOOQ 동시성(Concurrency) 제어 - Optimistic Lock / Pessimistic Lock

Pir·2022년 1월 23일
10
post-thumbnail

1. 동시성 제어(Concurrency Control)

왜 필요한가

  • 스프링 프레임워크는 기본적으로 멀티 쓰레드 환경이기 때문에 각기 다른 요청들에 의해 개발자가 원하지 않는 결과를 얻을 수 있다.

💡 해결 방안

  • 어플리케이션 사이드에서 동시성(쓰레드 기반)을 제어할 수도 있지만 이번 게시글에서는 Java 네이티브 쿼리빌더인 jOOQ를 활용하여 디비 사이드에서 동시성(트랜잭션 기반)을 제어하려고 합니다.

2. 동시성 제어 전 문제점

🔱 50개 쓰레드로 1개의 글 1번씩 조회

  • 락 걸지 않고 테스트
    public class NoLockWorker implements Runnable{
        private CountDownLatch countDownLatch;

        public NoLockWorker(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            postService.plusHitById(BASE_ID);
            countDownLatch.countDown();
        }
    }
    
    ------------------------------------------------------------------

    public void plusHitById(Long id){
        context.transaction(configuration ->{
            final Post post = DSL.using(configuration)
                                .selectFrom(POST)
                                .where(POST.ID.eq(id))
                                .fetchOneInto(Post.class);

            final Long hit = post.getHit();

            DSL.using(configuration)
                    .update(POST)
                    .set(POST.HIT, hit + 1L)
                    .where(POST.ID.eq(id))
                    .execute();
        });
    }
  • 결과

    테스트 실패
    => hit가 50개가 되는 것을 예상했지만, 50개의 멀티 쓰레드의 여러 트랜잭션이 동시에 한 ROW에 몰렸기 때문에 개발자가 원하지 않는 hit가 5라는 결과 도출

3. Optimistmic Lock / Pessimistic Lock

🔒 Optimistic Lock이란?

  • 트랜잭션이 충돌을 하지 않는다고 가정하고 가정합니다. (낙관적 접근)
  • version, timestamp 등 컬럼을 두어 해당 version에서 다른 트랜잭션이 업데이트했을 경우 예외 발생 (업데이트가 있을 경우 버전이 1씩 증가)
  • 업데이트 시점만에 락을 걸기 때문에 실시간 데이터를 보여줘야하는 비즈니스에 유리

🔏 Pessimistic Lock이란?

  • 트랜잭션이 충돌이 많이 발생한다고 가정하고 먼저 락을 겁니다. (비관적 접근)
  • 데이터베이스에서 제공하는 락을 사용합니다. Pessimistic Lock은 많은 종류가 있기 때문에 각 비즈니스마다 선택해서 사용 권장
  • Optimistic Lock과 비교하면 더 안정적인 방식이지만 실시간 데이터를 보여주는 비즈니스의 동시성과는 어울리지 않는다.

4. Optimistic Lock으로 동시성 제어

Version 컬럼 추가

  • Database (Post 테이블)

  • Gradle
    => jOOQ 설정 시 Gradle에서 사용할 version 컬럼을 지정할 수 있습니다.

DslContext 세팅시 withExecuteWithOptimisticLocking(true) 옵션을 추가한다.
또한, OptimisticLock이 충돌 때문에 예외를 던지면 커넥션을 계속 물고 있는 상황이 생기는데, try with resources를 사용하여 충돌 시 바로 커넥션을 종료시킨다.

    public void plusHitOptimisticById(Long id){
        try(Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD)){
            DSL.using(connection, SQLDialect.MYSQL, new Settings().withExecuteWithOptimisticLocking(true))
                    .transaction(configuration -> {
                        PostRecord postRecord = DSL.using(configuration)
                                                  .fetchOne(POST, POST.ID.eq(id));

                        postRecord.setHit(postRecord.getHit() + 1L);
                        postRecord.store();
                    });
        } catch (SQLException e) {
            log.info("error code : {}, error : {}", e.getErrorCode(), e);
        }
    }
  • 결과

    테스트 통과
    => 같은 버전에 2개 이상의 트랜잭션이 충돌하면 선행 트랜잭션의 업데이트만 성공하고 나머지 트랜잭션은 예외를 발생시킵니다. 해당 예외를 잡고 리트라이해서 동시성을 제어합니다.

5. Pessimistic Lock으로 동시성 제어

forUpdate를 활용한 Pessimistic Lock

forUpdate는 다른 트랜잭션의 CRUD 모두 허용하지 않는 특징이 있다.

  • DslContext 트랜잭션의 해당 데이터 조회 부분에 forUpdate라는 메서드를 추가한다.
  • 결과

    테스트 통과
    => 한 쓰레드 당 하나의 트랜잭션을 가지고 ROW에 접근하여 순서대로 조회 -> 업데이트하여 hit가 50이라는 결과를 도출할 수 있다.

느낀 점

💡

앞으로 동시성을 제어해야하 하는 상황에서, 어플리케이션 레벨에서 제어를 할지 데이터베이스 레벨에서 제어를 할지 조금은 더 고민해볼 수 있는 시간이 될 것 같습니다.
데이터베이스 레벨에서 제어한다고 하면 어떤 Lock이 더 비즈니스에 맞을 지 생각하고 선택할 수 있고, Optimistic Lock을 사용하면 어떤 식으로 구성해야할지 고민해볼 것 같습니다.
이번에 개인적으로 공부하면서 저희 파트에도 공유하고 회사 프로젝트에 적용했던 기회였기 때문에 더 재밌게 진행할 수 있었던 것 같습니다.

출처

https://www.jooq.org/doc/latest/manual/sql-execution/crud-with-updatablerecords/optimistic-locking/

profile
흉내내는 사람이 아닌, 이해하는 사람이 되자

2개의 댓글

comment-user-thumbnail
2022년 1월 23일

좋은 글 감사합니다. jooq 관련해서 조금 더 써주시면 감사할 것 같아요!

답글 달기
comment-user-thumbnail
2022년 2월 1일

글 잘 읽었습니다ㅎㅎ 전체 실행속도 또한 비관적 락보다 빠르게 나오나요?

db에 쓰기 쓰레드는 하나만 접근 할 수 있고 나머지는 다 버려지지는 않을지 궁금하네요!

답글 달기