Вступление
В моем предыдущем посте я представил механизм параллелизма кэша второго уровня 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
|
@Override public 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
|
@Override public 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
|
@Override public boolean isReadable( long txTimestamp) { return false ; } @Override public 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
|
@Override public boolean isReadable( long txTimestamp) { return txTimestamp > timestamp; } @Override public 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
|
@Override protected 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= 1 where id= 1 and version= 0 JdbcTransaction - rolled JDBC Connection 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 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 Connection 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 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 Connection 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 Cache 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 Влада Михалча в блоге Влада Михалчеа . |