Вступление
В моем предыдущем посте я представил механизм параллелизма кэша второго уровня NONSTRICT_READ_WRITE . В этой статье я собираюсь продолжить эту тему со стратегией READ_WRITE .
Сквозное кэширование
NONSTRICT_READ_WRITE — стратегия сквозного кэширования, которая обновляет аннулирующие записи кэша. Как бы ни была проста эта стратегия, производительность падает с увеличением операций записи. Стратегия сквозного кэширования является лучшим выбором для приложений с интенсивной записью, поскольку записи в кэше могут быть устаревшими, а не отбрасываться.
Поскольку база данных представляет собой систему записей, а операции базы данных заключены в физические транзакции, кэш может обновляться либо синхронно (как в случае стратегии параллелизма кэш-памяти TRANSACTIONAL ), либо асинхронно (сразу после фиксации транзакции базы данных).
Стратегия READ_WRITE представляет собой механизм одновременного асинхронного кэширования, и для предотвращения проблем целостности данных (например, устаревших записей кэша) она использует механизм блокировки, который обеспечивает гарантии изоляции на единицу работы .
Вставка данных
Поскольку постоянные сущности однозначно идентифицируются (каждая сущность присваивается отдельной строке базы данных), вновь созданные сущности кэшируются сразу после фиксации транзакции базы данных:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Overridepublic boolean afterInsert( Object key, Object value, Object version) throws CacheException { region().writeLock( key ); try { final Lockable item = (Lockable) region().get( key ); if ( item == null ) { region().put( key, new Item( value, version, region().nextTimestamp() ) ); return true; } else { return false; } } finally { region().writeUnlock( key ); }} |
Для того чтобы объект был кэширован при вставке, он должен использовать генератор SEQUENCE , кэш заполняется EntityInsertAction :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
@Overridepublic void doAfterTransactionCompletion(boolean success, SessionImplementor session) throws HibernateException { final EntityPersister persister = getPersister(); if ( success && isCachePutEnabled( persister, getSession() ) ) { final CacheKey ck = getSession() .generateCacheKey( getId(), persister.getIdentifierType(), persister.getRootEntityName() ); final boolean put = cacheAfterInsert( persister, ck ); } } postCommitInsert( success );} |
Генератор IDENTITY не очень хорошо работает с дизайном кэша первого уровня с транзакционной записью , поэтому связанная с ним EntityIdentityInsertAction не кэширует вновь вставленные записи (по крайней мере, пока HHH-7964 не будет исправлен).
Теоретически, между фиксацией транзакции базы данных и вставкой в кэш второго уровня одна параллельная транзакция может загрузить вновь созданную сущность, вызывая вставку в кэш. Хотя это возможно, задержка синхронизации кеша очень мала, и если параллельная транзакция перемежается, она только заставляет другую транзакцию попадать в базу данных вместо загрузки объекта из кеша.
Обновление данных
Хотя вставка сущностей является довольно простой операцией, для обновлений нам нужно синхронизировать как базу данных, так и запись в кэше. Стратегия параллелизма READ_WRITE использует механизм блокировки для обеспечения целостности данных:
- Процедура принятия Hibernate Transaction вызывает сброс сеанса
- EntityUpdateAction заменяет текущую запись в кэше объектом Lock
- Метод обновления используется для синхронных обновлений кэша, поэтому он ничего не делает при использовании асинхронной стратегии параллельного кэширования, такой как READ_WRITE.
- После фиксации транзакции базы данных вызываются обратные вызовы после завершения транзакции
- EntityUpdateAction вызывает метод afterUpdate объекта EntityRegionAccessStrategy.
- ReadWriteEhcacheEntityRegionAccessStrategy заменяет запись Lock фактическим элементом , инкапсулируя состояние рассеянного объекта
Удаление данных
Удаление объектов аналогично процессу обновления, как мы можем видеть из следующей диаграммы последовательности:
- Процедура принятия Hibernate Transaction вызывает сброс сеанса
- EntityDeleteAction заменяет текущую запись в кэше объектом Lock
- Вызов метода remove ничего не делает, поскольку READ_WRITE является стратегией асинхронного параллельного кэширования.
- После фиксации транзакции базы данных вызываются обратные вызовы после завершения транзакции
- EntityDeleteAction вызывает метод unlockItem объекта EntityRegionAccessStrategy
- ReadWriteEhcacheEntityRegionAccessStrategy заменяет запись Lock другим объектом Lock , период ожидания которого увеличен
После удаления объекта соответствующая ему запись кэша второго уровня будет заменена объектом Lock , который выполняет любой последующий запрос на чтение из базы данных вместо использования записи кэша.
Блокирующие конструкции
Классы Item и Lock наследуются от типа Lockable, и у каждого из них есть особая политика, позволяющая читать или записывать запись в кэше.
Объект READ_WRITE Lock
Класс Lock определяет следующие методы:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@Overridepublic boolean isReadable(long txTimestamp) { return false;}@Overridepublic boolean isWriteable(long txTimestamp, Object newVersion, Comparator versionComparator) { if ( txTimestamp > timeout ) { // if timedout then allow write return true; } if ( multiplicity > 0 ) { // if still locked then disallow write return false; } return version == null ? txTimestamp > unlockTimestamp : versionComparator.compare( version, newVersion ) < 0;} |
- Объект Lock не позволяет читать записи в кэше, поэтому любой последующий запрос должен идти в базу данных
- Если текущая временная метка создания сеанса превышает пороговое значение тайм-аута блокировки, запись в кэше разрешается записывать
- Если хотя бы одному сеансу удалось заблокировать эту запись, любая операция записи запрещена.
- Запись Lock позволяет писать, если состояние входящего объекта увеличило его версию или текущая временная метка создания сеанса больше, чем текущая временная метка разблокировки записи
Объект READ_WRITE Item
Класс Item определяет следующую политику доступа для чтения / записи:
|
01
02
03
04
05
06
07
08
09
10
11
|
@Overridepublic boolean isReadable(long txTimestamp) { return txTimestamp > timestamp;}@Overridepublic boolean isWriteable(long txTimestamp, Object newVersion, Comparator versionComparator) { return version != null && versionComparator .compare( version, newVersion ) < 0;} |
- Элемент доступен для чтения только из сеанса, который был запущен после времени создания записи в кэш.
- Запись Item позволяет писать, только если состояние входящего объекта увеличило свою версию
Контроль параллельного входа в кэш
Этот механизм управления параллелизмом вызывается при сохранении и чтении записей основного кэша.
Запись в кэш читается при вызове метода get ReadWriteEhcacheEntityRegionAccessStrategy :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public final Object get(Object key, long txTimestamp) throws CacheException { readLockIfNeeded( key ); try { final Lockable item = (Lockable) region().get( key ); final boolean readable = item != null && item.isReadable( txTimestamp ); if ( readable ) { return item.getValue(); } else { return null; } } finally { readUnlockIfNeeded( key ); }} |
Запись в кэш записывается методом ReadWriteEhcacheEntityRegionAccessStrategy putFromLoad :
|
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
|
public final boolean putFromLoad( Object key, Object value, long txTimestamp, Object version, boolean minimalPutOverride) throws CacheException { region().writeLock( key ); try { final Lockable item = (Lockable) region().get( key ); final boolean writeable = item == null || item.isWriteable( txTimestamp, version, versionComparator ); if ( writeable ) { region().put( key, new Item( value, version, region().nextTimestamp() ) ); return true; } else { return false; } } finally { region().writeUnlock( key ); }} |
Сроки
Если операция базы данных завершается неудачно, текущая запись в кэше содержит объект Lock и не может выполнить откат к своему предыдущему состоянию Item . По этой причине блокировка должна прерваться, чтобы позволить записи в кэше заменить действительный объект Item . EhcacheDataRegion определяет следующее свойство времени ожидания:
|
1
2
3
|
private static final String CACHE_LOCK_TIMEOUT_PROPERTY = "net.sf.ehcache.hibernate.cache_lock_timeout";private static final int DEFAULT_CACHE_LOCK_TIMEOUT = 60000; |
Если мы не переопределим свойство net.sf.ehcache.hibernate.cache_lock_timeout , тайм-аут по умолчанию составляет 60 секунд:
|
1
2
3
4
|
final String timeout = properties.getProperty( CACHE_LOCK_TIMEOUT_PROPERTY, Integer.toString( DEFAULT_CACHE_LOCK_TIMEOUT )); |
Следующий тест будет эмулировать неудачную транзакцию базы данных, поэтому мы можем наблюдать, как кэш READ_WRITE разрешает запись только после истечения порога времени ожидания. Сначала мы собираемся уменьшить значение тайм-аута, чтобы уменьшить период зависания кэша:
|
1
2
3
|
properties.put( "net.sf.ehcache.hibernate.cache_lock_timeout", String.valueOf(250)); |
Мы будем использовать пользовательский перехватчик для ручного отката текущей запущенной транзакции:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
@Overrideprotected Interceptor interceptor() { return new EmptyInterceptor() { @Override public void beforeTransactionCompletion( Transaction tx) { if(applyInterceptor.get()) { tx.rollback(); } } };} |
Следующая процедура протестирует поведение тайм-аута блокировки:
|
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
|
try { doInTransaction(session -> { Repository repository = (Repository) session.get(Repository.class, 1L); repository.setName("High-Performance Hibernate"); applyInterceptor.set(true); });} catch (Exception e) { LOGGER.info("Expected", e);}applyInterceptor.set(false);AtomicReference<Object> previousCacheEntryReference = new AtomicReference<>();AtomicBoolean cacheEntryChanged = new AtomicBoolean();while (!cacheEntryChanged.get()) { doInTransaction(session -> { boolean entryChange; session.get(Repository.class, 1L); try { Object previousCacheEntry = previousCacheEntryReference.get(); Object cacheEntry = getCacheEntry(Repository.class, 1L); entryChange = previousCacheEntry != null && previousCacheEntry != cacheEntry; previousCacheEntryReference.set(cacheEntry); LOGGER.info("Cache entry {}", ToStringBuilder.reflectionToString( cacheEntry)); if(!entryChange) { sleep(100); } else { cacheEntryChanged.set(true); } } catch (IllegalAccessException e) { LOGGER.error("Error accessing Cache", e); } });} |
Запуск этого теста приводит к следующему выводу:
|
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
select readwritec0_.id as id1_0_0_, readwritec0_.name as name2_0_0_, readwritec0_.version as version3_0_0_ from repository readwritec0_ where readwritec0_.id=1 update repository set name='High-Performance Hibernate', version=1where id=1 and version=0JdbcTransaction - rolled JDBC Connectionselect readwritec0_.id as id1_0_0_, readwritec0_.name as name2_0_0_, readwritec0_.version as version3_0_0_ from repository readwritec0_ where readwritec0_.id = 1Cache entry net.sf.ehcache.Element@3f9a0805[ key=ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest$Repository#1, value=Lock Source-UUID:ac775350-3930-4042-84b8-362b64c47e4b Lock-ID:0, version=1, hitCount=3, timeToLive=120, timeToIdle=120, lastUpdateTime=1432280657865, cacheDefaultLifespan=true,id=0]Wait 100 ms!JdbcTransaction - committed JDBC Connectionselect readwritec0_.id as id1_0_0_, readwritec0_.name as name2_0_0_, readwritec0_.version as version3_0_0_ from repository readwritec0_ where readwritec0_.id = 1 Cache entry net.sf.ehcache.Element@3f9a0805[ key=ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest$Repository#1, value=Lock Source-UUID:ac775350-3930-4042-84b8-362b64c47e4b Lock-ID:0, version=1, hitCount=3, timeToLive=120, timeToIdle=120, lastUpdateTime=1432280657865, cacheDefaultLifespan=true, id=0]Wait 100 ms!JdbcTransaction - committed JDBC Connectionselect readwritec0_.id as id1_0_0_, readwritec0_.name as name2_0_0_, readwritec0_.version as version3_0_0_ from repository readwritec0_ where readwritec0_.id = 1Cache entry net.sf.ehcache.Element@305f031[ key=ReadWriteCacheConcurrencyStrategyWithLockTimeoutTest$Repository#1, value=org.hibernate.cache.ehcache.internal.strategy.AbstractReadWriteEhcacheAccessStrategy$Item@592e843a, version=1, hitCount=1, timeToLive=120, timeToIdle=120, lastUpdateTime=1432280658322, cacheDefaultLifespan=true, id=0]JdbcTransaction - committed JDBC Connection |
- Первая транзакция пытается обновить сущность, поэтому соответствующая запись кэша второго уровня блокируется до совершения транзакции.
- Первая транзакция завершается неудачно и откатывается
- Блокировка удерживается, поэтому следующие две последовательные транзакции отправляются в базу данных, не заменяя запись Lock текущим состоянием загруженной сущности базы данных.
- После истечения времени ожидания блокировки третья транзакция может, наконец, заменить блокировку записью в кэше элементов (удерживая разобранное гидратированное состояние объекта )
Вывод
Стратегия параллелизма READ_WRITE предлагает преимущества механизма сквозного кэширования, но вы должны понимать его внутреннюю работу, чтобы решить, подходит ли он для ваших текущих требований доступа к данным проекта.
Для сценариев с интенсивной конкуренцией при записи конструкции блокировки будут вызывать попадание в базу данных других параллельных транзакций, поэтому вы должны решить, лучше ли подходит стратегия параллельного кэширования в этой ситуации.
- Код доступен на GitHub .
| Ссылка: | Как работает Hibernate READ_WRITE CacheConcurrencyStrategy от нашего партнера по JCG Влада Михалча в блоге Влада Михалчеа . |

