Как стать автором
Обновить

Hibernate Cache. Практика

Время на прочтение4 мин
Количество просмотров18K
Итак, в продолжение предыдущей статьи я попробую на реальных ситуациях рассказать о проблемах, которые возникали у меня при работе в реальных проектах.

Миграционные скрипты

Пожалуй, одной из наиболее частых проблем при работе с кешем в моем приложении является необходимость накатывать миграционные скрипты на работающий сервер. Ведь если эти скрипты запускаются не через фабрику сессий работающего сервера, то кеш этой фабрики никак не узнает об изменениях, которые делаются в базу. Следовательно, получаем проблему несовместимости данных. Для решения этой проблемы есть несколько путей:
  1. Рестарт сервера — самый простой и, обычно, самый не приемлемый способ;
  2. Очистка кеша через определенные механизмы — пожалуй самый оптимальный по простоте и надежности метод. Этот метод можно вынести, например в JMX, на веб страничку или другой интерфейс и вызывать при необходимости. Гибкость метода в том, что пишется это один раз, а используется сколько угодно и где угодно. В случае, если Ваш провайдер кеша — EHCache и класс провайдер — SingletonEhCacheProvider, то Ваш код может выглядеть так:
    public String dumpKeys() {
        String regions[] = CacheManager.getInstance().getCacheNames();
        StringBuilder allkeys = new StringBuilder();
        String newLine = System.getProperty("line.separator");
        for (String region : regions) {
            Ehcache cache = CacheManager.getInstance().getEhcache(region);
            allkeys.append(toSomeReadableString(cache.getKeys()));
            allkeys.append(newLine);
        }
        return allkeys.toString();
    }
    

    Естественно что этот код должен выполняться в том же процессе что и хибернейт, статистику которого Вы хотите отследить. Подробней можно прочитать тут. Того же можно добиться используя фабрику сессий.
  3. Запуск миграционных скриптов, используя фабрику сессий работающего сервера. Это похоже на второй метод, с той лишь разницей, что мы не очищаем кеш, а пропускаем все миграционные скрипты через существующую фабрику. Таким образом все необходимые кеши обновляться сами. Этот метод рационально использовать в случае если кеш большой и дешевле его обновлять нежели создавать по новой;


Кеш запросов

Кеш запросов, пожалуй, самый не эффективный из всех перечисленных в предыдущей статье. Этому есть несколько причин:
  1. Прежде всего (о чем я забыл упомянуть в первой статье) ключом к данным этого кеша выступает не только параметры запроса, но и сам запрос. Это особенно важно, когда запросов много и они большие.
  2. Кеш запросов очень часто сбрасывается. То есть, если хоть одна из таблиц, которые участвуют в запросе, была модифицирована, то кеш будет сброшен и запрос выполнен по новой.

Поэтому использовать его следует очень осторожно. И помните — нету смысла кешировать все подряд. Кешируйте только те запросы, которые действительно могут ускорить работу вашего приложения и те запросы, кеш для которых будет очень редко сбрасываться.
Типичный пример плохого места для кеша запроса — выборка количества чего-либо на ресурсе с высокой скоростью обновлений/добавлений сущностей. Скажем, Вам нужно вывести статистику созданных в приложении сущностей по их статусам и еще каким либо параметрам, например так:
    Criteria criteria = getSession().createCriteria(Plan.class);
    criteria.setProjection(Projections.projectionList()
        .add(Projections.groupProperty("status"))
        .add(Projections.rowCount())
     );
     criteria.setCacheable(true);

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

Удаление закешированных объектов

Я уже не могу вспомнить все конкретные обстоятельства проблем, что случались при работе в кешем. Но одну из них особенно хорошо помню — это удаление объектов, которые находятся в закешированной коллекции. Известно, что объекты и их зависимости кешируются отдельно. Следовательно, если у нас есть следующий класс:
@Entity
@Table(name = "shared_doc")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class SharedDoc{
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Set<User> users;
}

И один из пользователей был удален, то мы выполняем его удаление:
getSession().delete(user)

При этом, удаленная запись остается в кеше коллекции users. Получаем не консистентные данные. Это же справедливо для разного рода каскадных удалений. Когда удаленные в каскаде объекты остались в кеше. Одно из очевидных решений — удалять объекты также и из кеша коллекций в которых он может находится. То есть, в данном примере удаление должно выглядеть так:
SharedDoc doc = (SharedDoc) session.load(SharedDoc.class, 1L);
doc.getUsers().remove(user);
session.delete(user);

Это отлично работает когда user находится лишь в одном кеше коллекций — users. Но задача очень усложняется когда таких коллекций много и они могут быть раскиданы по разным сущностям. Подобного рода проблема также может привести к ObjectNotFoundException при попытке какого-либо действия с объектами, которые остались в кеше.

Конкурентные транзакции

Иногда кеш может вести себя не так как Вы ожидаете в случае конкурентных транзакций. Рассмотрим типичный случай:
Session session1 =  getSession();
Session session2 =  getSession());        

Transaction t = session1.beginTransaction();
Plan plan = (Plan) session1.load(Plan.class, 1L);
System.out.println (plan.getName());
plan.setName(newName);
t.commit();

t = session2.beginTransaction();
plan = (Plan) session2.load(Plan.class, 1L);
System.out.println (plan.getName());
tx2.commit();

session1.close();
session2.close();

Казалось бы, что при втором вызове плана он должен был бы быть получен из кеша второго уровня — но это не так. Так как объект session2 был создан до изменения объекта плана. И во время обращения к кешу второго уровня хибернейт сверяет время создания сессии и время последнего изменения объекта. Это поведение следует учитывать в Ваших приложениях, так как это может создать дополнительную нагрузку в тех местах, где Вы на это не рассчитываете.

Вот, пожалуй, и все из того, что мне удалось припомнить из проблем при работе с Hibernate Cache. К сожалению, с распределенным кешем я не работал и мне по этой теме нечего сказать. Если Вы сталкивались с другими проблемами не описанными в статье, комментируйте, буду добавлять.
Теги:
Хабы:
+19
Комментарии12

Публикации

Истории

Работа

Java разработчик
355 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн