Автор оригинала: Vlad Mihalcea.
В моем предыдущем посте я представил ТОЛЬКО для чтения стратегию CacheConcurrencyStrategy , которая является очевидным выбором для неизменяемых графов сущностей. Когда кэшированные данные изменчивы, нам нужно использовать стратегию кэширования для чтения и записи, и в этом посте будет описано, как работает кэш NONSTRICT_READ_WRITE второго уровня.
Когда транзакция Hibernate фиксируется, выполняется следующая последовательность операций:
Во-первых, кэш становится недействительным до того, как транзакция базы данных будет зафиксирована во время сброса:
- Текущая Транзакция гибернации (например, JdbcTransaction , Транзакция Jta ) сброшена
- DefaultFlushEventListener выполняет текущий Запрос действия
- EntityUpdateAction вызывает update метод EntityRegionAccessStrategy
- Стратегия доступа NonStrictReadWrite EhcacheCollectionRegion удаляет запись кэша из базового EhcacheEntityRegion
После фиксации транзакции базы данных запись в кэше снова удаляется:
- Текущая Транзакция гибернации после завершения вызывается обратный вызов
- Текущий Сеанс распространяет это событие на свой внутренний ActionQueue
- EntityUpdateAction вызывает метод AfterUpdate в EntityRegionAccessStrategy
- Стратегия доступа 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'}
- Алиса выбирает Репозиторий и обновляет его имя
- Обычай Перехватчик гибернации вызывается, и транзакция Боба запускается
- Поскольку Репозиторий был удален из Кэша , Боб загрузит кэш 2-го уровня с текущим снимком базы данных
- Транзакция Алисы фиксируется, но теперь Кэш содержит предыдущий снимок базы данных, который только что загрузил Боб
- Если третий пользователь теперь получит сущность Репозиторий , он также увидит устаревшую версию сущности, которая отличается от текущего снимка базы данных
- После фиксации транзакции Alice запись Кэш снова удаляется, и любой последующий запрос на загрузку сущности заполнит Кэш текущим снимком базы данных
Стратегия параллелизма NONSTRICT_READ_WRITE вводит крошечное окно несоответствия, когда база данных и кэш второго уровня могут не синхронизироваться. Хотя это может показаться ужасным, на самом деле мы всегда должны разрабатывать наши приложения, чтобы справляться с этими ситуациями, даже если мы не используем кэш второго уровня. Hibernate предлагает повторяемость на уровне приложений считывает данные через свой кэш первого уровня с обратной записью транзакций, и все управляемые объекты могут устареть. Сразу после загрузки сущности в текущий Контекст сохранения ее может обновить другая параллельная транзакция , и поэтому нам необходимо предотвратить эскалацию устаревших данных до потери обновлений .
Оптимистичный контроль параллелизма-эффективный способ борьбы с потерянными обновлениями в длинных разговорах , и этот метод также может смягчить проблему NONSTRICT_READ_WRITE несоответствия.
Стратегия параллелизма NONSTRICT_READ_WRITE является хорошим выбором для приложений, предназначенных в основном для чтения (при резервном копировании с помощью механизма оптимистической блокировки). В сценариях с интенсивной записью механизм аннулирования кэша увеличит частоту пропусков кэша , что сделает этот метод неэффективным.
Код доступен на GitHub .