Вступление
Как я уже объяснял ранее , корпоративное кэширование требует усердия. Поскольку данные дублируются между базой данных ( системой записи ) и уровнем кэширования, мы должны убедиться, что два отдельных источника данных не расходятся.
Если кэшированные данные являются неизменяемыми (ни база данных, ни кеш не могут их изменить), мы можем безопасно их кэшировать, не беспокоясь о проблемах согласованности. Данные только для чтения всегда являются хорошим кандидатом для кэширования на уровне приложений, повышая производительность чтения без необходимости ослаблять гарантии согласованности.
Кэширование второго уровня только для чтения
Для тестирования стратегии кэширования второго уровня только для чтения мы будем использовать следующую модель предметной области:
Репозиторий является корневым объектом и является родителем любого объекта 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 .
Ссылка: | Как работает Hibernate READ_ONLY CacheConcurrencyStrategy от нашего партнера по JCG Влада Михалча в блоге Влада Михалчи . |