Рубрики
Без рубрики

Как работает стратегия Hibernate READ_WRITE CacheConcurrencyStrategy

Автор оригинала: Vlad Mihalcea.

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

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

Поскольку база данных является системой записи и операции с базой данных заключены в физические транзакции кэш может обновляться либо синхронно (как в случае ТРАНЗАКЦИОННОЙ стратегии параллелизма кэша), либо асинхронно (сразу после фиксации транзакции базы данных).

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

Вставка данных

Поскольку сохраненные сущности идентифицируются однозначно (каждая сущность назначается отдельной строке базы данных), вновь созданные сущности кэшируются сразу после фиксации транзакции базы данных:

@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 );
    }
}

Чтобы объект был кэширован при вставке, он должен использовать генератор ПОСЛЕДОВАТЕЛЬНОСТЕЙ , кэш заполняется EntityInsertAction :

@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 );
}

Генератор ИДЕНТИФИКАТОРОВ не очень хорошо работает с транзакционной обработкой записи дизайном кэша первого уровня, поэтому связанная EntityIdentityInsertAction не кэширует вновь вставленные записи (по крайней мере, до тех пор, пока HHH-7964 не будет исправлена).

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

Обновление данных

Хотя вставка сущностей-довольно простая операция, для обновления нам необходимо синхронизировать как базу данных, так и запись в кэше. Стратегия параллелизма READ_WRITE использует механизм блокировки для обеспечения целостности данных:

  1. Процедура фиксации транзакции в режиме гибернации запускает сброс сеанса
  2. EntityUpdateAction заменяет текущую запись в кэше объектом Lock
  3. Метод update используется для синхронных обновлений кэша, поэтому он ничего не делает при использовании стратегии асинхронного параллелизма кэша, такой как READ_WRITE
  4. После фиксации транзакции базы данных вызываются обратные вызовы после завершения транзакции
  5. EntityUpdateAction вызывает AfterUpdate метод EntityRegionAccessStrategy
  6. Стратегия ReadWriteEhcacheEntityRegionAccessStrategy заменяет запись Lock фактической Элемент , инкапсулирующий сущность скрытое состояние

Удаление данных

Удаление сущностей аналогично процессу обновления, как мы видим на следующей диаграмме последовательности:

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

Запирающие конструкции

Классы Item и Lock наследуются от типа Lockable , и у каждого из этих двух есть определенная политика, позволяющая считывать или записывать запись в кэш.

Объект блокировки READ_WRITE

Класс Lock определяет следующие методы:

@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 не позволяет считывать запись в кэше, поэтому любой последующий запрос должен поступать в базу данных
  • Если отметка времени создания текущего сеанса превышает пороговое значение времени ожидания блокировки, запись в кэш разрешается записывать
  • Если хотя бы одному сеансу удалось заблокировать эту запись, любая операция записи запрещена
  • Запись Блокировка позволяет записывать, если состояние входящей сущности увеличило свою версию или отметка времени создания текущего сеанса больше, чем отметка времени разблокировки текущей записи

Объект элемента READ_WRITE

Класс Item определяет следующую политику доступа для чтения/записи:

@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;
}
  • Элемент доступен для чтения только из сеанса, который был запущен после времени создания записи в кэше
  • Запись Элемент позволяет записывать только в том случае, если состояние входящей сущности увеличило свою версию

Управление параллелизмом ввода в кэш

Этот механизм управления параллелизмом вызывается при сохранении и чтении базовых записей кэша.

Запись кэша считывается при вызове метода ReadWriteEhcacheEntityRegionAccessStrategy |/get :

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 :

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 . По этой причине блокировка должна пройти по тайм-ауту, чтобы запись в кэше была заменена фактической Предмет объект. Область данных Ehcache определяет следующее свойство тайм-аута:

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 секунды:

final String timeout = properties.getProperty(
    CACHE_LOCK_TIMEOUT_PROPERTY,
    Integer.toString( DEFAULT_CACHE_LOCK_TIMEOUT )
);

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

properties.put(
    "net.sf.ehcache.hibernate.cache_lock_timeout", 
    String.valueOf(250));

Мы будем использовать пользовательский перехватчик для ручного отката текущей транзакции:

@Override
protected Interceptor interceptor() {
    return new EmptyInterceptor() {
        @Override
        public void beforeTransactionCompletion(
            Transaction tx) {
            if(applyInterceptor.get()) {
                tx.rollback();
            }
        }
    };
}

Следующая процедура проверит поведение тайм-аута блокировки:

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 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);
        }
    });
}

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

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 .