Статьи

Как работает Hibernate NONSTRICT_READ_WRITE CacheConcurrencyStrategy

Вступление

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

Внутренние работы

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

nonstrictreadwritecacheconcurrencystrategy3

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

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

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

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

Предупреждение о несоответствии

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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);
});

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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));
        }
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
[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. Пользовательский Hibernate Interceptor вызывается и транзакция Боба запускается
  3. Поскольку хранилище было удалено из кэша , Боб загрузит кэш 2-го уровня с текущим снимком базы данных.
  4. Алиса транзакция фиксирует, но теперь Cache содержит предыдущий снимок базы данных, который Боб только что загрузил
  5. Если третий пользователь теперь получит объект Repository , он также увидит устаревшую версию объекта, которая отличается от текущего снимка базы данных.
  6. После фиксации транзакции Алисы запись Cache снова удаляется, и любой последующий запрос загрузки объекта заполняет Cache текущим снимком базы данных.

Устаревшие данные против потерянных обновлений

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

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

Вывод

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

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