JPAのロックとIsolationのテストとレビュー


用意する。


発端


知人にデータベースのDeadLockを体験したことがあるかと聞かれた.いいえ.これまで作成されたすべての機能はDedrockに対して処理されていません.
そこで,仮想デジタルコンピューティングアプリケーションにより,デッドマシンが発生した場合をシミュレートし,テストした.

n/a.環境


M1 MAC, JAVA 11, MySQL

開始します。


要求


一部のアプリケーションの数は、リクエストの数より1増加する必要があります.リクエストは、多くの人が同時にリクエストを送信し続けると仮定します.

n/a.理論


1.無防備


処理が行われていない場合、コンカレント要求はMySQLの基本的なIsolation REPEATABLE READとして使用されます.
したがって、トランザクションの開始時に読み込まれた値が0のままであり、N個のリクエストが同時に受信されたときの数字が0の場合、それらはすべて1に更新されます.

2.ロック-PESIMISTIC WRITE(悲観ロック、Xロック)


要求を満たすためにハングアップが頻繁に発生すると考えられ,選択時に「X錠」を掛け,順序を保証する.
=>ロックを共有できない理由:1つのトランザクションがロックを共有すると、別のトランザクションもロックを共有することができ、両方のトランザクションを読み取ることができます.
しかし、要求されたようにデータを修正する必要がある場合、排他的な攻撃を行う必要があるデータは修正できないため、排他的な攻撃を行う必要があるが、排他的な攻撃と共有的な攻撃を併用することができないため、死傷者が発生する.

3. Isolation - SERIALIZABLE


dedrockをシリアルで起動し、発生時にリフレッシュします.

アプリケーション構成

  • NumberEntity
    エンティティは構成されていますが、数値を増やすためにデータが生成されます.(IDが1に固定する)
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @Entity
    public class NumberEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
    
        private Long count;
    
        @Builder
        private NumberEntity(int id, Long count) {
            this.id = id;
            this.count = count;
        }
    
        public static NumberEntity of(int id, Long count){
            return NumberEntity.builder()
                    .id(id)
                    .count(count)
                    .build();
        }
    
        public Long increment(){
            return ++this.count;
        }
    
        public Long decrement(){
            return --this.count;
        }
    }
  • NumberRepository
    find関数は、従来のロックされたPESSIMISTIC WRITEおよびIsolationのシリアル化と同様の動作をそれぞれ実行する.
    (Isolationはサービス団が提供する)
  • @Repository
    public interface NumberRepository extends JpaRepository<NumberEntity, Integer> {
    
        Optional<NumberEntity> findById(int id);
    
        @Lock(LockModeType.PESSIMISTIC_WRITE)
        @Query("select n from NumberEntity n where n.id = :id")
        Optional<NumberEntity> findByNumberIdLock(@Param("id") int id);
    
        @Query("select n from NumberEntity n where n.id = :id")
        Optional<NumberEntity> findByNumberIdIsolation(@Param("id") int id);
    }
  • NumberService
  • が実現され、それぞれ理論1、2、および3に対応する.
    @RequiredArgsConstructor
    @Service
    @Slf4j
    public class NumberService {
    
        private final NumberRepository numberRepository;
    
        private final int numberId = 1;
    
        @Transactional
        public void incrementNumberNormal(){
            NumberEntity numberEntity = numberRepository.findById(numberId).orElseThrow(NoSuchElementException::new);
            System.out.println(Thread.currentThread().getName() + " : " + numberEntity.increment() + " " + numberEntity);
        }
    
        @Transactional
        public void incrementNumberLock() {
            NumberEntity numberEntity = numberRepository.findByNumberIdLock(numberId).orElseThrow(NoSuchElementException::new);
            System.out.println(Thread.currentThread().getName() + " : " + numberEntity.increment() + " " + numberEntity);
        }
    
        @Transactional(isolation = Isolation.SERIALIZABLE)
        public void incrementNumberSerializable(){
            NumberEntity numberEntity = numberRepository.findByNumberIdIsolation(numberId).orElseThrow(NoSuchElementException::new);
            System.out.println(Thread.currentThread().getName() + " : " + numberEntity.increment());
        }
    }

    りろんしけん


    テストは@SpringBootTestを使用してアプリケーションドライバ環境と連携して行います.また,要求を同時にテストするために,100スレッドを用いてそれぞれテストを行い,すべてのテストの数字は0から始まる.
    @SpringBootTest
    public class NumberServiceTest {
    
        private static final int COUNT = 100;
        private static final ExecutorService service = Executors.newFixedThreadPool(COUNT);
    
        @Autowired
        private NumberService numberService;
    }

    1.無防備

    @Test
    @DisplayName("요청 수 만큼 숫자 증가 (Normal)")
    void incrementNumber_normal() throws InterruptedException {
        // given
        CountDownLatch latch = new CountDownLatch(COUNT);
        Long before = numberService.getNumber();
    
        // when
        for (int i = 0; i < COUNT; ++i) {
            service.execute(() -> {
                numberService.incrementNumberNormal();
                latch.countDown();
            });
        }
        // then
        latch.await();
        assertEquals(before + COUNT, numberService.getNumber());
    }
    結果

    理論のように,100回のリクエストがあったが,実際に更新された数字は14であることがわかる.すべてのリクエストが数値を増加させることは保証されていません.

    2.ロック-PESIMISTIC WRITE(悲観ロック、Xロック)

    @Test
    @DisplayName("요청 수 만큼 숫자 증가 (Lock - PESSIMISTIC_WRITE)")
    void incrementNumber_concurrency() throws InterruptedException {
        // given
        CountDownLatch latch = new CountDownLatch(COUNT);
        Long before = numberService.getNumber();
        // when
        for (int i = 0; i < COUNT; ++i) {
            service.execute(() -> {
                numberService.incrementNumberLock();
                latch.countDown();
            });
        }
        // then
        latch.await();
        assertEquals(before + COUNT, numberService.getNumber());
    }

    理論に示すように、すべてのトランザクションはブロック順に処理され、正常に100増加しました.

    3. Isolation - SERIALIZABLE

    @Test
    @DisplayName("요청 수만 큼 증가 (Isolation - SERIALIZABLE)")
    void incrementNumber_Isolation() throws InterruptedException {
        // given
        CountDownLatch latch = new CountDownLatch(COUNT);
        Long before = numberService.getNumber();
        // when
        for (int i = 0; i < COUNT; ++i) {
            service.execute(() -> {
                numberService.incrementNumberSerializable();
                latch.countDown();
            });
        }
        // then
        latch.await();
        assertEquals(before + COUNT, numberService.getNumber());
    }

    死神が発生した.ただし、フリーズ後にリフレッシュを行っても、リクエストが継続的なアプリケーションであれば、リフレッシュが成功する保証はありません.状況は異なりますが、このアプリケーションのニーズでは、ドラック自体を生み出すのは適切ではないと思います.

    の最後の部分


    理論の中で最終的な選択をすると、ロックを選択します.
    しかし、資料を調べることで、鍵もアイソサレーションも慎重に使う内容を見ることができます.確かに、ロックの順序が保障されているのは利点かもしれませんが、リクエストが多くなると、いつまで待つことができません.特に、複数のサーバの構造では、なおさらです.
    今後の課題は,この同期性と整合性の均衡を状況に応じて決定するようである.