Статьи

Как Hibernate хранит записи кэша второго уровня

Вступление

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

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

Hibernate Persistence Context сам по себе не является решением для кэширования и служит не для повышения производительности операций чтения приложений, а для другой цели. Поскольку сеанс Hibernate привязан к текущей запущенной логической транзакции, после завершения транзакции сеанс уничтожается.

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

Правильное решение для кэширования должно охватывать несколько сеансов Hibernate, и именно поэтому Hibernate также поддерживает дополнительный кэш второго уровня . Кэш второго уровня привязан к жизненному циклу SessionFactory , поэтому он уничтожается только при закрытии SessionFactory (в частности, при закрытии приложения). Кэш второго уровня в основном ориентирован на сущности, хотя также поддерживает дополнительное решение для кэширования запросов.

По умолчанию кэш второго уровня отключен, и для его активации необходимо установить следующие свойства Hibernate :

1
2
3
4
properties.put("hibernate.cache.use_second_level_cache",
    Boolean.TRUE.toString());
properties.put("hibernate.cache.region.factory_class",
    "org.hibernate.cache.ehcache.EhCacheRegionFactory");

RegionFactory определяет поставщика реализации кэша второго уровня, и конфигурация hibernate.cache.region.factory_class является обязательной, если для свойства hibernate.cache.use_second_level_cache установлено значение true .

Чтобы включить кэширование на уровне сущностей, нам нужно аннотировать наши кэшируемые сущности следующим образом:

1
2
3
@Entity
@org.hibernate.annotations.Cache(usage =
    CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)

JPA также определяет аннотацию @Cacheable , но не поддерживает настройку стратегии параллелизма на уровне объекта.

Поток загрузки объекта

Всякий раз, когда объект должен быть загружен, запускается LoadEevent, и DefaultLoadEventListener обрабатывает его следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Object entity = loadFromSessionCache( event,
    keyToLoad, options );
if ( entity == REMOVED_ENTITY_MARKER ) {
    LOG.debug("Load request found matching entity
        in context, but it is scheduled for removal;
        returning null" );
    return null;
}
if ( entity == INCONSISTENT_RTN_CLASS_MARKER ) {
    LOG.debug("Load request found matching entity
        in context, but the matched entity was of
        an inconsistent return type;
        returning null"
    );
    return null;
}
if ( entity != null ) {
    if ( traceEnabled ) {
        LOG.tracev("Resolved object in "
            + "session cache: {0}",
            MessageHelper.infoString( persister,
                event.getEntityId(),
                event.getSession().getFactory() )
        );
    }
    return entity;
}
 
entity = loadFromSecondLevelCache( event,
    persister, options );
if ( entity != null ) {
    if ( traceEnabled ) {
        LOG.tracev("Resolved object in "
            + "second-level cache: {0}",
            MessageHelper.infoString( persister,
                event.getEntityId(),
                event.getSession().getFactory() )
        );
    }
}
else {
    if ( traceEnabled ) {
        LOG.tracev("Object not resolved in "
            + "any cache: {0}",
            MessageHelper.infoString( persister,
                event.getEntityId(),
                event.getSession().getFactory() )
        );
    }
    entity = loadFromDatasource( event, persister,
        keyToLoad, options );
}

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

Внутренние элементы кэша второго уровня

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

гидратация

В номенклатуре Hibernate гидратация — это когда JDBC ResultSet преобразуется в массив необработанных значений:

1
2
3
4
final Object[] values = persister.hydrate(
    rs, id, object,
    rootPersister, cols, eagerPropertyFetch, session
);

Гидратированное состояние сохраняется в текущем запущенном контексте постоянства как объект EntityEntry , который инкапсулировал моментальный снимок объекта во время загрузки. Гидратированное состояние затем используется:

  • механизм грязной проверки по умолчанию , который сравнивает текущие данные объекта со снимком времени загрузки
  • кэш второго уровня, записи которого создаются на основе моментального снимка объекта времени загрузки

Обратная операция называется дегидратацией и копирует состояние объекта в оператор INSERT или UPDATE .

Элементы кэша второго уровня

Хотя Hibernate позволяет нам манипулировать графами сущностей, кэш второго уровня использует вместо этого разобранное гидратированное состояние :

1
2
final CacheEntry entry = persister.buildCacheEntry(
    entity, hydratedState, version, session );

Гидратированное состояние разбирается перед сохранением в CacheEntry :

1
2
3
4
5
6
this.disassembledState = TypeHelper.disassemble(
    state, persister.getPropertyTypes(),
    persister.isLazyPropertiesCacheable()
        ? null : persister.getPropertyLaziness(),
    session, owner
);

Начиная со следующей диаграммы модели объекта:

postcommentdetailssecondlevelcache

Мы вставим следующие объекты:

1
2
3
4
5
6
7
8
Post post = new Post();
post.setName("Hibernate Master Class");
 
post.addDetails(new PostDetails());
post.addComment(new Comment("Good post!"));
post.addComment(new Comment("Nice post!"));
 
session.persist(post);

Теперь мы собираемся проверить каждый отдельный элемент кэша сущностей.

Элемент кэша сущности Post

Сущность Post имеет однозначную связь с сущностью Comment и обратную однозначную связь с PostDetails :

1
2
3
4
5
6
7
@OneToMany(cascade = CascadeType.ALL,
    mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
 
@OneToOne(cascade = CascadeType.ALL,
    mappedBy = "post", optional = true)
private PostDetails details;

При получении объекта Post :

1
Post post = (Post) session.get(Post.class, 1L);

Связанный элемент кэша выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
key = {org.hibernate.cache.spi.CacheKey@3855}
    key = {java.lang.Long@3860} "1"
    type = {org.hibernate.type.LongType@3861}
    entityOrRoleName = {java.lang.String@3862} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.SecondLevelCacheTest$Post"
    tenantId = null
    hashCode = 31
value = {org.hibernate.cache.spi.entry.StandardCacheEntryImpl@3856}
    disassembledState = {java.io.Serializable[3]@3864}
        0 = {java.lang.Long@3860} "1"
        1 = {java.lang.String@3865} "Hibernate Master Class"
    subclass = {java.lang.String@3862} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.SecondLevelCacheTest$Post"
    lazyPropertiesAreUnfetched = false
    version = null

CacheKey содержит идентификатор объекта, а CacheEntry содержит разобранное гидратированное состояние объекта.

Значение кэша записей записей состоит из столбца имени и идентификатора , который устанавливается ассоциацией комментариев « один ко многим» .

Ни взаимно-однозначные, ни обратные взаимно-однозначные ассоциации не встроены в Post CacheEntry .

Элемент кэша сущности PostDetails

Первичный ключ сущности PostDetails ссылается на связанный первичный ключ сущности Post , и поэтому он имеет непосредственную связь с сущностью Post .

1
2
3
4
@OneToOne
@JoinColumn(name = "id")
@MapsId
private Post post;

При получении сущности PostDetails :

1
2
PostDetails postDetails =
    (PostDetails) session.get(PostDetails.class, 1L);

Кэш второго уровня генерирует следующий элемент кеша:

01
02
03
04
05
06
07
08
09
10
11
12
key = {org.hibernate.cache.spi.CacheKey@3927}
    key = {java.lang.Long@3897} "1"
    type = {org.hibernate.type.LongType@3898}
    entityOrRoleName = {java.lang.String@3932} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.SecondLevelCacheTest$PostDetails"
    tenantId = null
    hashCode = 31
value = {org.hibernate.cache.spi.entry.StandardCacheEntryImpl@3928}
    disassembledState = {java.io.Serializable[2]@3933}
        0 = {java.sql.Timestamp@3935} "2015-04-06 15:36:13.626"
    subclass = {java.lang.String@3932} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.SecondLevelCacheTest$PostDetails"
    lazyPropertiesAreUnfetched = false
    version = null

В дизассемблированном состоянии содержится только свойство объекта selectedOn , поскольку идентификатор объекта встроен в CacheKey .

Элемент кэша сущности Comment

Сущность Comment имеет связь « многие к одному» с сообщением :

1
2
@ManyToOne
private Post post;

Когда мы получаем сущность Comment :

1
2
Comment comments =
    (Comment) session.get(Comment.class, 1L);

Hibernate генерирует следующий элемент кэша второго уровня:

01
02
03
04
05
06
07
08
09
10
11
12
13
key = {org.hibernate.cache.spi.CacheKey@3857}
    key = {java.lang.Long@3864} "2"
    type = {org.hibernate.type.LongType@3865}
    entityOrRoleName = {java.lang.String@3863} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.SecondLevelCacheTest$Comment"
    tenantId = null
    hashCode = 62
value = {org.hibernate.cache.spi.entry.StandardCacheEntryImpl@3858}
    disassembledState = {java.io.Serializable[2]@3862}
        0 = {java.lang.Long@3867} "1"
        1 = {java.lang.String@3868} "Good post!"
    subclass = {java.lang.String@3863} "com.vladmihalcea.hibernate.masterclass.laboratory.cache.SecondLevelCacheTest$Comment"
    lazyPropertiesAreUnfetched = false
    version = null

В разобранном состоянии содержатся ссылка на внешний ключ Post.id и столбец обзора , что отражает зеркальное определение таблицы базы данных.

Вывод

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

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

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

  • Код доступен на GitHub .