Рубрики
Без рубрики

Как работает стратегия Hibernate NONSTRICT_READ_WRITE CacheConcurrencyStrategy

Автор оригинала: Vlad Mihalcea.

В моем предыдущем посте я представил ТОЛЬКО для чтения стратегию CacheConcurrencyStrategy , которая является очевидным выбором для неизменяемых графов сущностей. Когда кэшированные данные изменчивы, нам нужно использовать стратегию кэширования для чтения и записи, и в этом посте будет описано, как работает кэш NONSTRICT_READ_WRITE второго уровня.

Когда транзакция Hibernate фиксируется, выполняется следующая последовательность операций:

Во-первых, кэш становится недействительным до того, как транзакция базы данных будет зафиксирована во время сброса:

  1. Текущая Транзакция гибернации (например, JdbcTransaction , Транзакция Jta ) сброшена
  2. DefaultFlushEventListener выполняет текущий Запрос действия
  3. EntityUpdateAction вызывает update метод EntityRegionAccessStrategy
  4. Стратегия доступа NonStrictReadWrite EhcacheCollectionRegion удаляет запись кэша из базового EhcacheEntityRegion

После фиксации транзакции базы данных запись в кэше снова удаляется:

  1. Текущая Транзакция гибернации после завершения вызывается обратный вызов
  2. Текущий Сеанс распространяет это событие на свой внутренний ActionQueue
  3. EntityUpdateAction вызывает метод AfterUpdate в EntityRegionAccessStrategy
  4. Стратегия доступа NonStrictReadWrite EhcacheCollectionRegion вызывает метод remove для базового EhcacheEntityRegion

Режим NONSTRICT_READ_WRITE не является стратегией кэширования на основе записи, а является режимом параллелизма кэша на основе чтения , поскольку записи в кэше становятся недействительными, а не обновляются. Аннулирование кэша не синхронизировано с текущей транзакцией базы данных. Даже если связанная запись Кэш регион будет признана недействительной дважды (до и после завершения транзакции), все равно остается крошечное временное окно, когда кэш и база данных могут разойтись.

Следующий тест продемонстрирует эту проблему. Во-первых, мы определим логику транзакций Алисы:

doInTransaction(session -> {
    LOGGER.info("Load and modify Repository");
    Repository repository = (Repository)
        session.get(Repository.class, 1L);
    assertTrue(getSessionFactory().getCache()
        .containsEntity(Repository.class, 1L));
    repository.setName("High-Performance Hibernate");
    applyInterceptor.set(true);
});

endLatch.await();

assertFalse(getSessionFactory().getCache()
    .containsEntity(Repository.class, 1L));

doInTransaction(session -> {
    applyInterceptor.set(false);
    Repository repository = (Repository)
        session.get(Repository.class, 1L);
    LOGGER.info("Cached Repository {}", repository);
});

Алиса загружает объект Репозиторий и изменяет его в своей первой транзакции базы данных. Чтобы создать еще одну параллельную транзакцию прямо тогда, когда Алиса готовится к фиксации, мы собираемся использовать следующий Спящий режим Перехватчик :

private AtomicBoolean applyInterceptor = 
    new AtomicBoolean();

private final CountDownLatch endLatch = 
    new CountDownLatch(1);

private class BobTransaction extends EmptyInterceptor {
    @Override
    public void beforeTransactionCompletion(Transaction tx) {
        if(applyInterceptor.get()) {
            LOGGER.info("Fetch Repository");

            assertFalse(getSessionFactory().getCache()
                .containsEntity(Repository.class, 1L));

            executeSync(() -> {
                Session _session = getSessionFactory()
                    .openSession();
                Repository repository = (Repository) 
                    _session.get(Repository.class, 1L);
                LOGGER.info("Cached Repository {}", 
                    repository);
                _session.close();
                endLatch.countDown();
            });

            assertTrue(getSessionFactory().getCache()
                .containsEntity(Repository.class, 1L));
        }
    }
}

При выполнении этого кода генерируется следующий вывод:

[Alice]: Load and modify Repository
[Alice]: select nonstrictr0_.id as id1_0_0_, nonstrictr0_.name as name2_0_0_ from repository nonstrictr0_ where nonstrictr0_.id=1
[Alice]: update repository set name='High-Performance Hibernate' where id=1

[Alice]: Fetch Repository from another transaction
[Bob]: select nonstrictr0_.id as id1_0_0_, nonstrictr0_.name as name2_0_0_ from repository nonstrictr0_ where nonstrictr0_.id=1
[Bob]: Cached Repository from Bob's transaction Repository{id=1, name='Hibernate-Master-Class'}

[Alice]: committed JDBC Connection

[Alice]: select nonstrictr0_.id as id1_0_0_, nonstrictr0_.name as name2_0_0_ from repository nonstrictr0_ where nonstrictr0_.id=1
[Alice]: Cached Repository Repository{id=1, name='High-Performance Hibernate'}
  1. Алиса выбирает Репозиторий и обновляет его имя
  2. Обычай Перехватчик гибернации вызывается, и транзакция Боба запускается
  3. Поскольку Репозиторий был удален из Кэша , Боб загрузит кэш 2-го уровня с текущим снимком базы данных
  4. Транзакция Алисы фиксируется, но теперь Кэш содержит предыдущий снимок базы данных, который только что загрузил Боб
  5. Если третий пользователь теперь получит сущность Репозиторий , он также увидит устаревшую версию сущности, которая отличается от текущего снимка базы данных
  6. После фиксации транзакции Alice запись Кэш снова удаляется, и любой последующий запрос на загрузку сущности заполнит Кэш текущим снимком базы данных

Стратегия параллелизма NONSTRICT_READ_WRITE вводит крошечное окно несоответствия, когда база данных и кэш второго уровня могут не синхронизироваться. Хотя это может показаться ужасным, на самом деле мы всегда должны разрабатывать наши приложения, чтобы справляться с этими ситуациями, даже если мы не используем кэш второго уровня. Hibernate предлагает повторяемость на уровне приложений считывает данные через свой кэш первого уровня с обратной записью транзакций, и все управляемые объекты могут устареть. Сразу после загрузки сущности в текущий Контекст сохранения ее может обновить другая параллельная транзакция , и поэтому нам необходимо предотвратить эскалацию устаревших данных до потери обновлений .

Оптимистичный контроль параллелизма-эффективный способ борьбы с потерянными обновлениями в длинных разговорах , и этот метод также может смягчить проблему NONSTRICT_READ_WRITE несоответствия.

Стратегия параллелизма NONSTRICT_READ_WRITE является хорошим выбором для приложений, предназначенных в основном для чтения (при резервном копировании с помощью механизма оптимистической блокировки). В сценариях с интенсивной записью механизм аннулирования кэша увеличит частоту пропусков кэша , что сделает этот метод неэффективным.

Код доступен на GitHub .