JAVA

индекс
156,47

Hibernate Cache. Практика

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

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

Пожалуй, одной из наиболее частых проблем при работе с кешем в моем приложении является необходимость накатывать миграционные скрипты на работающий сервер. Ведь если эти скрипты запускаются не через фабрику сессий работающего сервера, то кеш этой фабрики никак не узнает об изменениях, которые делаются в базу. Следовательно, получаем проблему несовместимости данных. Для решения этой проблемы есть несколько путей:
  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
19 января 2012, 19:12
94

комментарии (12)

0
vip_delete #
>необходимость накатывать миграционные скрипты на работающий сервер

Например?
+1
sskorykh #
Ха, миграционный скрипты… Проблема с кэшем возникает гораздо чаще, чем кажется.

Многие программисты, разрабатывающие ПО на заказ, не имеют опыта практической эксплуатации собственного ПО. Сдали проект, получили деньги, забыли. А практика — великая вещь! На практике такие ситуации бывают, какие программисту при разработке и не снились.

К примеру, любая программа может отработать некорректно. Последствия можно оперативно исправить SQL-запросами, но из-за кэша сервер эти изменения не увидит. Функционал любой программы может оказаться недостаточным. Например, клиент грохает собственный заказ, звонит и слёзно просит его восстановить. В программе такая возможность не была предусмотрена. Казалось бы, проблема решается в один SQL-запрос, ан нет, для сброса кэша приходится останавливать сервер.

Попав несколько раз в подобные ситуации я стал использовать кэширование крайне, крайне осторожно.
0
doom369 #
Немного промахнулся. Ответил Вам ниже
0
doom369 #
Ну, например, возьмем простой случай — у Вас в приложении для новой сущности проставляется флаг DRAFT. А заказчик хочет изменить DRAFT на NEW (не только labels но и внутри кода).
Тогда делаем update и сбрасываем кеш.
0
vip_delete #
У сущности переименовывается поле с draft на new и еще код изменяется? Так тут точно перезагрузка сервера нужна, а если она не приемлема, то делается два сервера с одним балансировщиком и эти два сервера по очереди перезагружаются: так поступают все сайты, которые должны работать 24x7. Никак не могу понять зачем что-то с кешем делать, бизнес-логике должно быть без разницы: есть или нет кеша, она должна в обоих случаях работать одинаково.
0
doom369 #
Ну ок, код пока трогать не будем, допустим просто выполнили

update plan set status = 'new' where status = 'draft'

объекты в кеше при этом остались со значениями draft
0
vip_delete #
Такие запросы можно из админки сайта выполнять через сессию, тогда кеш, если он есть, сам обновится. Эти миграционные скрипты какие-то особенные, что их надо через sql проводить? Так sql-запросы можно и через сессию проводить тоже.
0
doom369 #
Ну так я как раз и описал в статье возможные варианты. Каждый выбирает то, что ему больше всего подходит.
0
doom369 #
Для сброса кеша есть решения, которые я описал в начале статьи, делается это за час максимум. Потом можно использовать во всех проектах.
По поводу удаления данных — в своих проектах я всегда использую флаг — deleted. Реально из базы не удаляю. Как раз для решения таких проблем как Вы описали.
+2
sskorykh #
Ну в общем-то мне надо было чётче сказать: тема, поднятая в статье, на самом деле весьма актуальна.

Если останов сервера является неприемлемым, то надо реализовать возможность принудительного сброса кэша из админки сервера. Даже если кажется, что эта возможность лишняя, вероятность, что она окажется востребованной весьма высока.

А вообще в плане кэширования мне нравится подход, реализованный в iBatis. Там можно гибко настроить время жизни данных в кэше. Для справочников сделать побольше, для оперативных данных — поменьше. В результате сторонние изменения сервер подхватывает достаточно быстро, и в пике нагрузке даёт хорошую производительность.

А из базы я тоже ничего не удаляю, потому восстановление сводится к операции типа «update orders set deleted = null where id = ?». Главное, чтобы сервер в курсе был )
0
doom369 #
По поводу iBatis — в EHCache такая же гибкая настройка с помощью регионов. Нужен кеш для статики — пожалуйста, нужен кеш для одного типа объектов — пожалуйста. Все гибко. Статика, кстати, у меня не сбрасывается.
0
zloyreznic #
Хорошо описали проблемы…
Думал внедрить в проект, сейчас уже думаю «А надо?»
Спасибо

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.