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() — очищает весь кеш.

    Заключение

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

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

      +2
      Спасибо за хорошую информацию на русском.
      Эх… есть одна хорошая статья на эту же тему, но в новом дизайне блога её как-то сильно «поплющило» :(
      acupof.blogspot.com/2008/01/background-hibernate-comes-with-three.html
      Много полезной информации в самом MindMap'е, но текст с картинок сейчас абсолютно не читаем.
      Возможно у кого-то осталась старая версия?
      0
      Скажите, а кеш запросов работает непосредственно из контекста видимости одного метода?
        0
        Кеш запросов — на уровне фабрики сессий. Но у него есть свои механизмы инвалидации, про это написано по ссылке в первом комментарии.
        0
        А что будет, если у объекта есть свойство, которое в свою очередь тоже объект, хранящийся в другой таблице + в маппинге прописано, что нужно использовать outer join для его выборки?
          0
          Если оба класса будут помечены аннотацией для кеширования, то оба объекта будут закешированы, несмотря на то что, запрос 1 с джоином.
            0
            Я имел в виду что-то такое.

            @Entity
            @Table(name = "shared_doc")
            public class SharedDoc{
            private User Owner;
            }

            т.е. кеширование свойства явно не задано, но ведь кеш первого уровня работает всегда.

            Далее срабатывает такой код:

            SharedDoc user = (User) session.load(User.class, 1L);
            SharedDoc persistedDoc = (SharedDoc) session.load(SharedDoc.class, 10L);


            При этом известно, что у SharedDoc c id=10 стоит ownerId=1, плюс в маппинге для SharedDoc задано извлечение свойства Owner через join. Какой запрос будет выполнен? Вообще это не сильно важный вопрос, просто интересно )
              0
              У Вас ошибка — SharedDoc user.
              По вопросу — я думаю зависит от того как будет выглядеть мапинг SharedDoc в классе User.
                0
                С телефона оч неудобно участвовать в дискуссиях ((
                А как то может зависеть от маппига? Что может повлиять?
                  0
                  Да, в этом конкретном вопросе — это важно.
                  Вообщем если попытаться ответить баз маппинга, то должно быть приблизительно так —
                  если при session.load(User.class, 1L) делается выборка и User и SharedDoc (отдельными селектами или джоинами) то оба объекта будут в кеше. И во втором запросе значение будет доставаться из кеша.
          0
          Если кеш первого уровня нельзя отключить, то как hibernate контролирует соответствие данных в кеше данным в базе?
            0
            Не совсем понятен вопрос. Можно точнее сформулировать? Или описать гипотетическую ситуацию
              0
              1) считали запись с id=1, она попала в кеш первого уровня
              2) другое приложение обновило эту запись
              3) еще раз обращаемся за данными с id=1, они возвращаются из кеша
                0
                Вопрос ко второму пункту — как именно обновило? Через ту же сессию что была открыта в пункте 1? Через ту же фабрику сессий или напрямую в БД =)?
                  0
                  Другое приложение. Читай напрямую в БД.
                    0
                    Если так, то если на момент шага 3 мы имеем ту же сессию что и в случае 1, то данные будут не консистентны. Если же на шаге 3 у нас открывается новая сессия то все будет ок.
                      0
                      Даже если бы не было кеша первого уровня, то результат зависит ещё и от уровня изоляции в БД.
                      Если уровень изоляции Repeatable read или Serializable, то даже с отключенным кешем первого уровня вы получите одни и те же данные в одной транзакции, но разные в разных транзакциях.
                        0
                        Это понятно, я просто слабо знаком с hibernate и поэтому меня смутило то что кеш первого уровня отключить нельзя. Сейчас разобрался. Есть flush() и evict() для удаления объекта из кеша сессии.
                          0
                          о! ты же не запостился вчера!
                          0
                          Ну собственно это очевидно. Собственно я сперва удивился что, неужели в этой библиотеке нельзя сделать запрос мимо кеша. Сейчас уже сам разобрался.
                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

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

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