hibernate查询缓存
我已经很长时间没有使用过Hibernate了,甚至更久没有写过博客了 。 最近,我在建立常绿缓存的工作范围内撰写博客文章。 在编写演示代码时,我遇到了一些与Hibernate的查询缓存有关的问题:它没有按我预期的那样工作。 终于,一段时间后,我设法解决了这个问题。
这篇文章旨在更深入地研究Hibernate的Query Cache,以帮助遇到同样问题的其他开发人员。
实体缓存
Hibernate为不同的对象提供了不同类型的缓存。 其中主要的是实体 :
实体表示使用容器管理的持久性自动存储在关系数据库中的持久性数据。 它们之所以具有持久性,是因为它们的数据被持久地存储在某种形式的数据存储系统(例如数据库)中:它们在服务器故障,故障转移或网络故障后仍然可以幸免。 重新实例化实体后,前一个实例的状态将自动恢复。
Hibernate提供了一个两层的实体高速缓存,它的名称不合逻辑,称为Level 1 Cache和Level 2 Cache。
-
一级缓存
-
L1C默认情况下处于启用状态-无法禁用AFAIK。 它由
Session
对象自动管理。 缓存的生命周期绑定到会话的生命周期。在标准的Web应用程序中,将为每个HTTP请求打开一个Hibernate
Session
。 这意味着每个新请求都从冷缓存开始。 因此,需要从数据库中重新加载数据。
二级缓存
-
L2C需要显式启用。 它依赖于第三方缓存解决方案, 例如 EHCache,JCache, Hazelcast等。Hibernate为提供程序提供了SPI,供实现使用以与框架进行接口。 缓存功能取决于实现, 即存储格式,是否分发数据等。
L2C通过
SessionFactory
管理:在Web应用程序中,单个实例在启动时被初始化。 这意味着与L1C相反,L2C可以(并且)在多个HTTP请求中使用。换句话说,只有L2C会对性能产生影响:因为默认情况下L2C处于禁用状态,并且会跨请求进行缓存。
实体缓存用于两种不同的情况:
- 当实体通过其主键加载时 。 在调用
EntityManager.find(Class<T> clazz, Object primaryKey)
方法时,就是这种情况。 使用Spring Data JPA,后者由CrudRepository.findById(ID id)
方法实现包装。 - 当
EntityManager
任何其他方法生成SELECT
查询时。 在许多情况下, 例如使用任何createXXXQuery()
方法或使用类型更安全的CriteriaBuilder
时,createXXXQuery()
发生这种情况。 对于Spring Data JPA,在JPA存储库中添加的任何自定义方法都是这种情况。
在第一种情况下被加载并在第二种情况下被查询的实体存储在缓存中。 但是,仅在第一种情况下才从缓存中读取实体。 考虑以下示例:
@Entity
@Cache(region="entities",usage=READ_WRITE)
classThing(@Idvalid:Long,valtext:String)
interfaceThingRepository:JpaRepository<Thing,Long>
@SpringBootApplication
classSample{
@Bean
funinit(repo:ThingRepository)=CommandLineRunner(){
repo.findAll() (1)
repo.findById(1L) (2)
}
}
- 所有实体均从数据库加载,并存储在缓存中
- 具有PK
1L
实体将从缓存中加载-如果存在
现在,让我们更改init()
函数的实现:
@Bean
funclr(repo:ThingRepository)=CommandLineRunner(){
repo.findAll() (1)
repo.findAll() (2)
}
- 所有实体均从数据库加载,并存储在缓存中
- 尽管所有实体都在缓存中,但并未使用:实体仍从数据库中加载
查询缓存
实际上,可以从常规SELECT
查询中读取缓存的结果,上述第二种情况。 这需要一个额外的缓存,即查询缓存 。 启用查询缓存分为两个步骤:
- 正确启用查询缓存。 例如,在Spring Boot中,只需将以下内容添加到
application.yml
:spring: jpa: properties: hibernate: cache: use_query_cache:true
- 为每个需要缓存的查询启用查询缓存:对于Spring Data JPA,必须使用
@QueryHints(QueryHint(name = HINT_CACHEABLE, value = "true"))
注释每个查询方法。 如果该方法不是自定义的, 即它已经由父级JpaRepository
提供, 例如findAll()
,则需要对其进行覆盖,并覆盖方法:interfaceThingRepository:JpaRepository<Thing,Long>{ @QueryHints(QueryHint(name=HINT_CACHEABLE,value="true")) overridefunfindAll():List<Thing> }
使用与上述相同的代码,结果将从缓存中返回,并且将无法访问数据库:
@Bean
funclr(repo:ThingRepository)=CommandLineRunner(){
repo.findAll() (1)
repo.findAll() (2)
}
- 所有实体均从数据库加载,并存储在缓存中
- 实体将从缓存中返回
样本演示项目
我使用Spring Data JPA和Spring Shell创建了一个简单的Spring Boot 演示项目 。 L2C和查询缓存以及Hibernate统计信息均已启用。 该项目提供了几个命令:
-
entities
使用findAll()
方法从数据库中读取所有实体 -
cache()
显示L2C的内容 -
queryCache()
显示查询缓存的内容
让我们按顺序使用它们:
- 启动后,L2C为空:
shell:> cache
+--+----+---------+ |id|text|timestamp| +--+----+---------+
- 查询缓存也为空:
shell:> query-cache
+-----+----+ |query|keys| +-----+----+
- 现在,使用
findAll()
方法加载实体。shell:> entities
+--+----+ |id|text| +--+----+ |1 |Foo | |2 |Bar | |3 |Baz | +--+----+
- 让我们确保现在已填充L2C:
shell:> cache
+----------------------------------+----+----------------+ |id |text|timestamp | +----------------------------------+----+----------------+ |ch.frankel.blog.querycache.Thing#1|Foo |6508639341051904| |ch.frankel.blog.querycache.Thing#2|Bar |6508639341240320| |ch.frankel.blog.querycache.Thing#3|Baz |6508639341244416| +----------------------------------+----+----------------+
- 查询缓存也是如此:
shell:> query-cache
+------------------------------------------------------------------------------+-------+ |query |keys | +------------------------------------------------------------------------------+-------+ |sql: select thing0_.id as id1_0_, thing0_.text as text2_0_ from thing thing0_;|[1,2,3]| |parameters: ; | | |named parameters: {}; | | |transformer: [email protected] | | +------------------------------------------------------------------------------+-------+
请注意,查询缓存不存储实体本身,而仅存储其主键。 然后从L2C加载实体。
- 可以通过再次调用
entities
命令并查看Hibernate统计信息来确认缓存行为:
Session Metrics { 20810 nanoseconds spent acquiring 1 JDBC connections; 0 nanoseconds spent releasing 0 JDBC connections; 0 nanoseconds spent preparing 0 JDBC statements; 0 nanoseconds spent executing 0 JDBC statements; 0 nanoseconds spent executing 0 JDBC batches; 0 nanoseconds spent performing 0 L2C puts; 1171505 nanoseconds spent performing 4 L2C hits; 2443442 nanoseconds spent performing 1 L2C misses; 0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections); 2799 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections) }
它提到了4个缓存命中:一个是查询缓存,另一个是L2C。
结论
缓存是一个折衷方案:它通过接受缓存的数据可能过时来提高性能。 当缓存本身是更新基础数据库的唯一路径时,陈旧数据的风险为零。
L2C可以从缓存中返回单个实体。 查询缓存允许批量返回它们。 考虑在项目中将后者与前者一起使用,以立即提高性能。
致谢:
感谢我的朋友Vlad“ Vladuts” Mihalcea在审阅这篇文章时所提供的帮助。
更进一步:
hibernate查询缓存