Hibernate cache

    Довольно часто в java приложениях с целью снижения нагрузки на БД используют кеш. Не много людей реально понимают как работает кеш под капотом, добавить просто аннотацию не всегда достаточно, нужно понимать как работает система. Поэтому этой статье я попытаюсь раскрыть тему про то, как работает кеш популярного ORM фреймворка. Итак, для начала немного теории.

    Прежде всего Hibernate cache — это 3 уровня кеширования:
    • Кеш первого уровня (First-level cache);
    • Кеш второго уровня (Second-level cache);
    • Кеш запросов (Query cache);

    Кеш первого уровня

    Кеш первого уровня всегда привязан к объекту сессии. Hibernate всегда по умолчанию использует этот кеш и его нельзя отключить. Давайте сразу рассмотрим следующий код:
    SharedDoc persistedDoc = (SharedDoc) session.load(SharedDoc.class, docId);
    System.out.println(persistedDoc.getName());
    user1.setDoc(persistedDoc);
    
    persistedDoc = (SharedDoc) session.load(SharedDoc.class, docId);
    System.out.println(persistedDoc.getName());
    user2.setDoc(persistedDoc);
    

    Возможно, Вы ожидаете, что будет выполнено 2 запроса в БД? Это не так. В этом примере будет выполнен 1 запрос в базу, несмотря на то, что делается 2 вызова load(), так как эти вызовы происходят в контексте одной сессии. Во время второй попытки загрузить план с тем же идентификатором будет использован кеш сессии.
    Один важный момент — при использовании метода load() Hibernate не выгружает из БД данные до тех пор пока они не потребуются. Иными словами — в момент, когда осуществляется первый вызов load, мы получаем прокси объект или сами данные в случае, если данные уже были в кеше сессии. Поэтому в коде присутствует getName() чтобы 100% вытянуть данные из БД. Тут также открывается прекрасная возможность для потенциальной оптимизации. В случае прокси объекта мы можем связать два объекта не делая запрос в базу, в отличии от метода get(). При использовании методов save(), update(), saveOrUpdate(), load(), get(), list(), iterate(), scroll() всегда будет задействован кеш первого уровня. Собственно, тут нечего больше добавить.

    Кеш второго уровня

    Если кеш первого уровня привязан к объекту сессии, то кеш второго уровня привязан к объекту-фабрике сессий (Session Factory object). Что как бы подразумевает, что видимость этого кеша гораздо шире кеша первого уровня. Пример:
    Session session = factory.openSession();
    SharedDoc doc = (SharedDoc) session.load(SharedDoc.class, 1L);
    System.out.println(doc.getName());
    session.close();
    
    session = factory.openSession();
    doc = (SharedDoc) session.load(SharedDoc.class, 1L);   
    System.out.println(doc.getName());       
    session.close();    
    

    В данном примере будет выполнено 2 запроса в базу, это связано с тем, что по умолчанию кеш второго уровня отключен. Для включения необходимо добавить следующие строки в Вашем конфигурационном файле JPA (persistence.xml):
    <property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletonEhCacheProvider"/>
    //или  в более старых версиях
    //<property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
    <property name="hibernate.cache.use_second_level_cache" value="true"/>
    

    Обратите внимание на первую строку. На самом деле, хибернейт сам не реализует кеширование как таковое. А лишь предоставляет структуру для его реализации, поэтому подключить можно любую реализацию, которая соответствует спецификации нашего ORM фреймворка. Из популярных реализаций можна выделить следующие:
    • EHCache
    • OSCache
    • SwarmCache
    • JBoss TreeCache

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

    Только после всех этих манипуляций кеш второго уровня будет включен и в примере выше будет выполнен только 1 запрос в базу.
    Еще одна важная деталь про кеш второго уровня про которую стоило бы упомянуть — хибернейт не хранит сами объекты Ваших классов. Он хранит информацию в виде массивов строк, чисел и т. д. И идентификатор объекта выступает указателем на эту информацию. Концептуально это нечто вроде Map, в которой id объекта — ключ, а массивы данных — значение. Приблизительно можно представить себе это так:
    1 -> { "Pupkin", 1, null , {1,2,5} }
    

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


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

    Кеш запросов

    Перепишем первый пример так:
    Query query = session.createQuery("from SharedDoc doc where doc.name = :name");
    
    SharedDoc persistedDoc = (SharedDoc) query.setParameter("name", "first").uniqueResult();
    System.out.println(persistedDoc.getName());
    user1.setDoc(persistedDoc);
    
    persistedDoc = (SharedDoc) query.setParameter("name", "first").uniqueResult();
    System.out.println(persistedDoc.getName());
    user2.setDoc(persistedDoc);
    

    Результаты такого рода запросов не сохраняются ни кешом первого, ни второго уровня. Это как раз то место, где можно использовать кеш запросов. Он тоже по умолчанию отключен. Для включения нужно добавить следующую строку в конфигурационный файл:
    <property name="hibernate.cache.use_query_cache" value="true"/>
    

    а также переписать пример выше добавив после создания объекта Query (то же справедливо и для Criteria):
    Query query = session.createQuery("from SharedDoc doc where doc.name = :name");
    query.setCacheable(true);
    

    Кеш запросов похож на кеш второго уровня. Но в отличии от него — ключом к данным кеша выступает не идентификатор объекта, а совокупность параметров запроса. А сами данные — это идентификаторы объектов соответствующих критериям запроса. Таким образом, этот кеш рационально использовать с кешем второго уровня.

    Стратегии кеширования

    Стратегии кеширования определяют поведения кеша в определенных ситуациях. Выделяют четыре группы:
    • Read-only
    • Read-write
    • Nonstrict-read-write
    • Transactional

    Подробней можно прочитать тут.

    Cache region

    Регион или область — это логический разделитель памяти вашего кеша. Для каждого региона можна настроить свою политику кеширования (для EhCache в том же ehcache.xml). Если регион не указан, то используется регион по умолчанию, который имеет полное имя вашего класса для которого применяется кеширование. В коде выглядит так:
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "STATIC_DATA")
    

    А для кеша запросов так:
    query.setCacheRegion("STATIC_DATA");
    //или в случае критерии
    criteria.setCacheRegion("STATIC_DATA");
    


    Что еще нужно знать?

    Во время разработки приложения, особенно сначала, очень удобно видеть действительно ли кешируются те или иные запросы, для этого нужно указать фабрике сессий следующие свойства:
    <property name="hibernate.show_sql" value="true"/>
    <property name="hibernate.format_sql" value="true"/>
    

    В дополнение фабрика сессий также может генерировать и сохранять статистику использования всех объектов, регионов, зависимостей в кеше:
    <property name="hibernate.generate_statistics" value="true"/>
    <property name="hibernate.cache.use_structured_entries" value="true"/>
    

    Для этого есть объекты Statistics для фабрики и SessionStatistics для сессии.

    Методы сессии:
    flush() — синхронизирует объекты сессии с БД и в то же время обновляет сам кеш сессии.
    evict() — нужен для удаления объекта из кеша cессии.
    contains() — определяет находится ли объект в кеше сессии или нет.
    clear() — очищает весь кеш.

    Заключение

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

    Подробнее
    Реклама

    Комментарии 28

      +2
      Спасибо за хорошую информацию на русском.
      Эх… есть одна хорошая статья на эту же тему, но в новом дизайне блога её как-то сильно «поплющило» :(
      acupof.blogspot.com/2008/01/background-hibernate-comes-with-three.html
      Много полезной информации в самом MindMap'е, но текст с картинок сейчас абсолютно не читаем.
      Возможно у кого-то осталась старая версия?
      0
      Скажите, а кеш запросов работает непосредственно из контекста видимости одного метода?
        0
        Кеш запросов — на уровне фабрики сессий. Но у него есть свои механизмы инвалидации, про это написано по ссылке в первом комментарии.
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Если оба класса будут помечены аннотацией для кеширования, то оба объекта будут закешированы, несмотря на то что, запрос 1 с джоином.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              У Вас ошибка — SharedDoc user.
              По вопросу — я думаю зависит от того как будет выглядеть мапинг SharedDoc в классе User.
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Да, в этом конкретном вопросе — это важно.
                  Вообщем если попытаться ответить баз маппинга, то должно быть приблизительно так —
                  если при session.load(User.class, 1L) делается выборка и User и SharedDoc (отдельными селектами или джоинами) то оба объекта будут в кеше. И во втором запросе значение будет доставаться из кеша.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Не совсем понятен вопрос. Можно точнее сформулировать? Или описать гипотетическую ситуацию
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Вопрос ко второму пункту — как именно обновило? Через ту же сессию что была открыта в пункте 1? Через ту же фабрику сессий или напрямую в БД =)?
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Если так, то если на момент шага 3 мы имеем ту же сессию что и в случае 1, то данные будут не консистентны. Если же на шаге 3 у нас открывается новая сессия то все будет ок.
                      0
                      Даже если бы не было кеша первого уровня, то результат зависит ещё и от уровня изоляции в БД.
                      Если уровень изоляции Repeatable read или Serializable, то даже с отключенным кешем первого уровня вы получите одни и те же данные в одной транзакции, но разные в разных транзакциях.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • НЛО прилетело и опубликовало эту надпись здесь
                        • НЛО прилетело и опубликовало эту надпись здесь
                0
                1. На самом деле, хибернейт сам не реализует кеширование как таковое
                2. Хибернейт не хранит сами объекты Ваших классов. Он хранит информацию в виде массивов строк, чисел и т. д. И идентификатор объекта выступает указателем на эту информацию. Концептуально это нечто вроде Map, в которой id объекта — ключ, а массивы данных — значение. Приблизительно можно представить себе это так:
                1 -> { «Pupkin», 1, null, {1,2,5} }

                Как первое вяжется со вторым? И если второе — истина, как hibernate восстанавливает объект, если, например, для этого требуются некие хитрые действия?

                PS Насколько я помню проксирование объектов можно отключить.
                  0
                  Отлично вяжется. Хибернейт достает объект из базы, скажем, в виде списка полей. И если провайдер кеша видит необходимость кеширования, то кеширует этот объект (который в виду списка) у себя, так как хибернейт передает ссылку на этот объект провайдеру. Как то так.

                  Как хибернейт восстанавливает объект из списка я не знаю. Нужно смотреть код.
                    0
                    Наверное, глупо звучит, но забыл что Hibernate сам маппирует объект с представлением в базе.
                      0
                      Не, он не передаёт ссылку. Он передаёт уже поля объекта, а не сам объект — так называемые dehydrated entity. Это сделано для того, чтобы не хранить объекты (т.к. кеш второго уровня может быть и не в вашем адресном пространстве приложения), а так же для того, чтобы испытывать меньше проблем с рассинхронизацией данных между БД, кешем и разными сессиями Hibernate
                    0
                    Начиная с версии Hibernate 3.3 кэш 2-го уровня включается так:

                    Вместо hibernate.cache.provider_class нужно указывать hibernate.cache.region.factory_class. И тут 2 варианта:
                    net.sf.ehcache.hibernate.EhCacheRegionFactory либо синглетон: net.sf.ehcache.hibernate.SingletonEhCacheRegionFactory

                    Пруфлинк: Ehcache documentation
                      0
                      Точно, спасибо. Вообще не обратил на это внимание.
                        0
                        Так (hibernate.cache.region.factory_class) включается кеш, если в зависимостях непосредственно ehcache-core В случае старого доброго hibernate-ehcache Нужно использовать hibernate.cache.provider_class

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

                    Самое читаемое