Hibernate READ_WRITE CacheConcurrencyStrategy如何工作

介绍

在我以前的文章中,我介绍了NONSTRICT_READ_WRITE二级缓存并发机制。 在本文中,我将使用READ_WRITE策略继续本主题。

直写式缓存

NONSTRICT_READ_WRITE是一种通读缓存策略,可更新最终无效的缓存条目。 尽管这种策略可能很简单,但是随着写入操作的增加,性能会下降。 对于需要大量写入的应用程序,直写式高速缓存策略是更好的选择,因为高速缓存条目可以被日期化而不是被丢弃。

因为数据库是记录系统,并且数据库操作被包装在物理事务中,所以可以同步更新缓存(例如TRANSACTIONAL缓存并发策略的情况)或异步更新(在提交数据库事务之后)。

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

对于要在插入时进行缓存的实体,它必须使用SEQUENCE生成器 ,该缓存由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 );
}

IDENTITY生成器不能与事务性的后写式第一级缓存设计配合使用,因此关联的EntityIdentityInsertAction不会缓存新插入的条目(至少在修复HHH-7964之前)。

从理论上讲,在数据库事务提交和第二级高速缓存插入之间,一个并发事务可能会加载新创建的实体,因此触发高速缓存插入。 虽然可能,但缓存同步滞后非常短,如果并发事务被交错,则只会使另一个事务命中数据库,而不是从缓存中加载实体。

更新资料

尽管插入实体是一个相当简单的操作,但是对于更新,我们需要同步数据库和缓存条目。 READ_WRITE并发策略采用锁定机制来确保数据完整性:

readwritecacheconcurrencystrategy_update4

  1. Hibernate Transaction提交过程触发会话刷新
  2. EntityUpdateActionLock对象替换当前缓存条目
  3. update方法用于同步缓存更新,因此在使用异步缓存并发策略(如READ_WRITE)时不会执行任何操作
  4. 提交数据库事务 ,将调用after-transaction-completion回调
  5. EntityUpdateAction调用EntityRegionAccessStrategyafterUpdate方法
  6. ReadWriteEhcacheEntityRegionAccessStrategyLock条目替换为实际的Item ,从而封装了实体分解状态

删除资料

从下面的序列图中可以看出,删除实体与更新过程类似:

readwritecacheconcurrencystrategy_delete3

删除实体后,其关联的二级缓存条目将被一个Lock对象代替,该对象将发出任何随后的请求以从数据库读取而不是使用缓存条目。

锁定构造

ItemLock类都继承自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对象不允许读取缓存条目,因此任何后续请求都必须发送到数据库
  • 如果当前会话创建时间戳大于“锁定超时”阈值,则允许写入缓存条目
  • 如果至少一个会话设法锁定了该条目,则禁止进行任何写操作
  • 如果进入的实体状态已增加其版本,或者当前的会话创建时间戳大于当前的条目解锁时间戳,则可以使用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;
}
  • 仅在缓存条目创建时间之后启动的会话中才可读取项目
  • Item条目仅在传入实体状态已增加其版本时才允许写入
缓存条目并发控制

当保存和读取底层缓存条目时,将调用这些并发控制机制。

在调用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状态。 由于这个原因,锁必须超时,以允许将缓存条目替换为实际的Item对象。 EhcacheDataRegion定义以下超时属性:

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

运行此测试将生成以下输出:

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条目
  • 在Lock超时期限到期后,第三笔交易最终可以用Item缓存条目替换Lock (保持实体分解为水合状态

结论

READ_WRITE并发策略具有直写式缓存机制的优点,但是您需要了解它的内部工作原理,才能确定它是否适合您当前的项目数据访问要求。

对于繁重的写争用方案,锁定结构将使其他并发事务进入数据库,因此您必须确定同步高速缓存并发策略是否更适合这种情况。

翻译自: https://www.javacodegeeks.com/2015/05/how-does-hibernate-read_write-cacheconcurrencystrategy-work.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值