Статьи

Как работает Hibernate READ_ONLY CacheConcurrencyStrategy

Вступление

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

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

Кэширование второго уровня только для чтения

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

repositorycommitchangereadonlycacheconcurrencystrategy

Репозиторий является корневым объектом и является родителем любого объекта Commit . Каждый коммит имеет список компонентов Change (встраиваемые типы значений).

Все объекты кэшируются как элементы только для чтения:

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

Постоянные сущности

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

1
2
3
4
5
doInTransaction(session -> {
    Repository repository =
        new Repository("Hibernate-Master-Class");
    session.persist(repository);
});

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testRepositoryEntityLoad() {
    LOGGER.info("Read-only entities are read-through");
 
    doInTransaction(session -> {
        Repository repository = (Repository)
            session.get(Repository.class, 1L);
        assertNotNull(repository);
    });
 
    doInTransaction(session -> {
        LOGGER.info("Load Repository from cache");
        session.get(Repository.class, 1L);
    });
}

Этот тест генерирует вывод:

01
02
03
04
05
06
07
08
09
10
11
12
--Read-only entities are read-through
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1
 
--JdbcTransaction - committed JDBC Connection
 
--Load Repository from cache
 
--JdbcTransaction - committed JDBC Connection

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

Обновление сущностей

Записи кэша только для чтения не могут быть обновлены. Любая такая попытка заканчивается исключением:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testReadOnlyEntityUpdate() {
    try {
        LOGGER.info("Read-only cache entries cannot be updated");
        doInTransaction(session -> {
            Repository repository = (Repository)
                session.get(Repository.class, 1L);
            repository.setName(
                "High-Performance Hibernate"
            );
        });
    } catch (Exception e) {
        LOGGER.error("Expected", e);
    }
}

Запуск этого теста приводит к следующему выводу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
--Read-only cache entries cannot be updated
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1
 
UPDATE repository
SET    NAME = 'High-Performance Hibernate'
WHERE  id = 1
 
--JdbcTransaction - rolled JDBC Connection
 
--ERROR Expected
--java.lang.UnsupportedOperationException: Can't write to a readonly object

Поскольку объекты кэша, доступные только для чтения, практически неизменны, рекомендуется назначать им аннотацию @Immutable, специфичную для Hibernate .

Удаление сущностей

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
public void testReadOnlyEntityDelete() {
    LOGGER.info("Read-only cache entries can be deleted");
    doInTransaction(session -> {
        Repository repository = (Repository)
            session.get(Repository.class, 1L);
        assertNotNull(repository);
        session.delete(repository);
    });
    doInTransaction(session -> {
        Repository repository = (Repository)
            session.get(Repository.class, 1L);
        assertNull(repository);
    });
}

Генерация следующего вывода:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
--Read-only cache entries can be deleted
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;
 
DELETE FROM repository
WHERE  id = 1
 
--JdbcTransaction - committed JDBC Connection
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;
 
--JdbcTransaction - committed JDBC Connection

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

Кэширование коллекций

У объекта Commit есть коллекция компонентов Change .

1
2
3
4
5
6
@ElementCollection
@CollectionTable(
    name="commit_change",
    joinColumns=@JoinColumn(name="commit_id")
)
private List<Change> changes = new ArrayList<>();

Хотя сущность Commit кэшируется как элемент только для чтения, коллекция Change игнорируется кэшем второго уровня.

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
@Test
public void testCollectionCache() {
    LOGGER.info("Collections require separate caching");
    doInTransaction(session -> {
        Repository repository = (Repository)
            session.get(Repository.class, 1L);
        Commit commit = new Commit(repository);
        commit.getChanges().add(
            new Change("README.txt", "0a1,5...")
        );
        commit.getChanges().add(
            new Change("web.xml", "17c17...")
        );
        session.persist(commit);
    });
    doInTransaction(session -> {
        LOGGER.info("Load Commit from database");
        Commit commit = (Commit)
            session.get(Commit.class, 1L);
        assertEquals(2, commit.getChanges().size());
    });
    doInTransaction(session -> {
        LOGGER.info("Load Commit from cache");
        Commit commit = (Commit)
            session.get(Commit.class, 1L);
        assertEquals(2, commit.getChanges().size());
    });
}

Запуск этого теста приводит к следующему выводу:

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
--Collections require separate caching
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;
 
 
INSERT INTO commit
            (id, repository_id)
VALUES      (DEFAULT, 1);
              
INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '0a1,5...', 'README.txt');      
 
INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '17c17...', 'web.xml');
              
--JdbcTransaction - committed JDBC Connection
 
--Load Commit from database
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;
 
 
SELECT changes0_.commit_id AS commit_i1_0_0_,
       changes0_.diff      AS diff2_1_0_,
       changes0_.path      AS path3_1_0_
FROM   commit_change changes0_
WHERE  changes0_.commit_id = 1
 
--JdbcTransaction - committed JDBC Connection
 
--Load Commit from cache
 
SELECT changes0_.commit_id AS commit_i1_0_0_,
       changes0_.diff      AS diff2_1_0_,
       changes0_.path      AS path3_1_0_
FROM   commit_change changes0_
WHERE  changes0_.commit_id = 1
 
--JdbcTransaction - committed JDBC Connection

Хотя сущность Commit извлекается из кэша, коллекция Change всегда выбирается из базы данных. Поскольку Изменения также являются неизменяемыми, мы хотели бы также кэшировать их, чтобы сохранить ненужные обращения к базе данных.

Включение поддержки кэша коллекции

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

1
2
3
4
5
6
7
8
9
@ElementCollection
@CollectionTable(
    name="commit_change",
    joinColumns=@JoinColumn(name="commit_id")
)
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_ONLY
)
private List<Change> changes = new ArrayList<>();

Повторное выполнение предыдущего теста приводит к следующему выводу:

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
--Collections require separate caching
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;
 
 
INSERT INTO commit
            (id, repository_id)
VALUES      (DEFAULT, 1);
              
INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '0a1,5...', 'README.txt');      
 
INSERT INTO commit_change
            (commit_id, diff, path)
VALUES      (1, '17c17...', 'web.xml');
              
--JdbcTransaction - committed JDBC Connection
 
--Load Commit from database
 
SELECT readonlyca0_.id   AS id1_2_0_,
       readonlyca0_.NAME AS name2_2_0_
FROM   repository readonlyca0_
WHERE  readonlyca0_.id = 1;
 
 
SELECT changes0_.commit_id AS commit_i1_0_0_,
       changes0_.diff      AS diff2_1_0_,
       changes0_.path      AS path3_1_0_
FROM   commit_change changes0_
WHERE  changes0_.commit_id = 1
 
--JdbcTransaction - committed JDBC Connection
 
--Load Commit from cache
 
--JdbcTransaction - committed JDBC Connection

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

Вывод

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

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