介绍
在我以前的文章中 ,我介绍了READ_ONLY CacheConcurrencyStrategy ,这是不可变实体图的显而易见的选择。 当高速缓存的数据可变时,我们需要使用读写高速缓存策略,本文将介绍NONSTRICT_READ_WRITE二级高速缓存的工作方式。
内部运作
提交Hibernate事务后,将执行以下操作序列:
首先,在刷新期间,在提交数据库事务之前,缓存无效:
- 当前的Hibernate事务 (例如JdbcTransaction , JtaTransaction )已刷新
- DefaultFlushEventListener执行当前的ActionQueue
- EntityUpdateAction调用EntityRegionAccessStrategy的更新方法
- NonStrictReadWriteEhcacheCollectionRegionAccessStrategy从基础EhcacheEntityRegion中删除缓存条目
提交数据库事务后,将再次删除缓存条目:
- 完成回调后当前的Hibernate Transaction被调用
- 当前会话将此事件传播到其内部ActionQueue
- EntityUpdateAction在EntityRegionAccessStrategy上调用afterUpdate方法
- NonStrictReadWriteEhcacheCollectionRegionAccessStrategy调用基础EhcacheEntityRegion上的remove方法
不一致警告
NONSTRICT_READ_WRITE模式不是“ 直 写式”缓存策略,因为缓存条目无效,而不是被更新。 缓存无效化与当前数据库事务不同步。 即使关联的Cache区域条目两次无效(在事务完成之前和之后),当缓存和数据库可能分开时,仍然存在一个很小的时间窗口。
以下测试将演示此问题。 首先,我们将定义Alice事务逻辑:
doInTransaction(session -> {LOGGER.info("Load and modify Repository");Repository repository = (Repository)session.get(Repository.class, 1L);assertTrue(getSessionFactory().getCache().containsEntity(Repository.class, 1L));repository.setName("High-Performance Hibernate");applyInterceptor.set(true);
});endLatch.await();assertFalse(getSessionFactory().getCache().containsEntity(Repository.class, 1L));doInTransaction(session -> {applyInterceptor.set(false);Repository repository = (Repository)session.get(Repository.class, 1L);LOGGER.info("Cached Repository {}", repository);
});
爱丽丝加载一个存储库实体,并在她的第一个数据库事务中对其进行修改。
为了在Alice准备提交时产生另一个并发事务,我们将使用以下Hibernate Interceptor :
private AtomicBoolean applyInterceptor = new AtomicBoolean();private final CountDownLatch endLatch = new CountDownLatch(1);private class BobTransaction extends EmptyInterceptor {@Overridepublic void beforeTransactionCompletion(Transaction tx) {if(applyInterceptor.get()) {LOGGER.info("Fetch Repository");assertFalse(getSessionFactory().getCache().containsEntity(Repository.class, 1L));executeSync(() -> {Session _session = getSessionFactory().openSession();Repository repository = (Repository) _session.get(Repository.class, 1L);LOGGER.info("Cached Repository {}", repository);_session.close();endLatch.countDown();});assertTrue(getSessionFactory().getCache().containsEntity(Repository.class, 1L));}}
}
运行此代码将生成以下输出:
[Alice]: Load and modify Repository
[Alice]: select nonstrictr0_.id as id1_0_0_, nonstrictr0_.name as name2_0_0_ from repository nonstrictr0_ where nonstrictr0_.id=1
[Alice]: update repository set name='High-Performance Hibernate' where id=1[Alice]: Fetch Repository from another transaction
[Bob]: select nonstrictr0_.id as id1_0_0_, nonstrictr0_.name as name2_0_0_ from repository nonstrictr0_ where nonstrictr0_.id=1
[Bob]: Cached Repository from Bob's transaction Repository{id=1, name='Hibernate-Master-Class'}[Alice]: committed JDBC Connection[Alice]: select nonstrictr0_.id as id1_0_0_, nonstrictr0_.name as name2_0_0_ from repository nonstrictr0_ where nonstrictr0_.id=1
[Alice]: Cached Repository Repository{id=1, name='High-Performance Hibernate'}
- Alice获取存储库并更新其名称
- 调用定制的Hibernate Interceptor并启动Bob的事务
- 由于存储库已从缓存中逐出,因此Bob会使用当前数据库快照加载第二级缓存
- Alice事务提交,但是现在缓存包含Bob刚刚加载的先前数据库快照
- 如果第三位用户现在将获取存储库实体,则他还将看到与当前数据库快照不同的陈旧实体版本。
- 提交Alice事务后,将再次逐出Cache条目,并且任何后续实体加载请求都将使用当前数据库快照填充Cache
过时的数据与丢失的更新
当数据库和二级缓存可能不同步时, NONSTRICT_READ_WRITE并发策略会引入一个很小的不一致窗口。 尽管这听起来可能很糟糕,但实际上,即使我们不使用二级缓存,也应始终设计应用程序来应对这些情况。 Hibernate通过其事务性的后台写式第一级缓存提供应用程序级可重复读取,并且所有托管实体都将变得过时。 在实体加载到当前的持久性上下文中之后 ,另一个并发事务可能会对其进行更新,因此,我们需要防止陈旧数据升级为丢失更新 。
乐观并发控制是处理长时间对话中丢失的更新的有效方法,该技术还可以缓解NONSTRICT_READ_WRITE不一致问题。
结论
NONSTRICT_READ_WRITE并发策略是大多数只读应用程序的理想选择(如果由乐观锁定机制支持)。 对于写密集型方案,缓存无效机制将增加缓存未命中率 ,因此使该技术效率低下。
- 代码可在GitHub上获得 。
翻译自: https://www.javacodegeeks.com/2015/05/how-does-hibernate-nonstrict_read_write-cacheconcurrencystrategy-work.html