💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 1 第三方缓存插件 除了Ehcache这种轻量级的缓存方案外,几乎所有IMDG产品都提供了对Hibernate二级缓存的直接支持,常用的有: Ø  Hazelcast Ø  GridGain Ø  JBoss Infinispan Ø  Terracotta(额外提供了直接替换Session对象的集成方式) ### 2 缓存工作过程 下面以JVM集群Terracotta为例,首先从最原始的JDBC到Hibernate到开启Hibernate二级缓存,看一下应用对数据库请求的情况。 ![](https://box.kancloud.cn/2016-08-31_57c6b139b3718.jpg) ### 2.1 自动提交模式下的JDBC 在自动提交模式下使用JDBC时,JDBC不会对请求有任何缓存,每次SQL操作都会直接发送到数据库。因此下图中三个用户的访问会导致9次数据库访问。 ![](https://box.kancloud.cn/2016-08-31_57c6b139cd3ee.jpg) ### 2.2 Hibernate一级缓存 Hibernate有两级缓存(参考各种Hibernate进行复习),默认情况下第二级缓存是不开启的,后面会看到导致的问题。所以默认情况下,Hibernate会开启事务,并缓存更新操作,在最后事务提交时一起更新到数据库。算上commit事务提交的话,一共就是6次数据库访问。一级缓存在Session关闭时会自动清除,或者应用通过evict()清除某个对象、clear()清除全部缓存内容、flush()同步缓存与数据库。此外,与二级缓存相同的一点注意是:**使用HQL或SQL查询属性级别的数据时,是不会使用缓存的**。(第三部分缓存工作原理中会详细解释) ![](https://box.kancloud.cn/2016-08-31_57c6b139dfc8f.jpg) ### 2.3 集成Hibernate二级缓存 Hibernate的二级缓存进一步将多个Session的数据加载请求缓存。然而风险也随之而来,当开启二级缓存后Hibernate不再每次都请求数据库,于是缓存中的数据可能是过期的。有时我们可以使用TTL设置来强制Hibernate刷新,但有的场景下这种方案也是不可行的。此时,就可以使用Terracotta将这些二级缓存形成集群,Terracotta负责集群结点间数据的同步。这种方式只需改动Hibernate配置文件即可,但由于Hibernate对二级缓存的使用方式的限制,这种集成方式并不能最大限度地发挥Terracotta的威力。 ![](https://box.kancloud.cn/2016-08-31_57c6b139f4034.jpg) ### 2.4 集成Hibernate Session Terracotta与Hibernate最快的集成方式就是直接与Hibernate的Session对象集成。因为POJO对象在Hibernate的二级缓存中实际是以字节数组的形式保存的,因此尽管已经在缓存中了,但也有序列化的开销。而使用这种集成Hibernate Session的方式,应用代码可以零延迟地直接访问内存中的POJO对象,没有任何多余开销。同样,我们也能够零延迟地在内存中写POJO对象。此时,数据库成了系统的历史记录,只在需要时更新。 ![](https://box.kancloud.cn/2016-08-31_57c6b13a1711a.jpg) ### 3 缓存内部工作原理 ### 3.1 二级缓存 之所以叫二级缓存是因为当你打开session时,Hibernate会自动为你开启一个一级缓存。官方文档中对二级缓存是这样描述的: A Hibernate Session is a transaction-level cache of persistent data. It is possible to configure a cluster or JVM-level (SessionFactory-level) cache on a class-by-class and collection-by-collection basis. You may even plug in a clustered cache. Be careful. Caches are never aware of changes made to the persistent store by another application (though they may be configured to regularly expire cached data). 像上面所提到的,只要SessionFactory开启着,二级缓存就存在。二级缓存持有每个标记为缓存的实体的所有**属性**和**关联**。下面以Person实体为例,说明一下二级缓存的内部工作原理: ![](https://box.kancloud.cn/2016-08-31_57c6b13a2d1e5.jpg) 我们先不开启关联的缓存,来看一下基本属性的缓存。二级缓存中,Person对象的保存格式如下:key是id,value是firstName,middleInitial,lastName三个String,外加parent的id。所以很明显,对象的实例没有直接保存在缓存中,而是被拆分成一个个属性保存,Hibernate将这个过程称为对象**脱水(dehydrate)**,反之属性组装成对象的过程称为补水(hydrate)。Hibernate为什么这样做? Ø  用户代码对实体的实例上的修改不会破坏缓存,因为缓存里都是实例属性的拷贝。 Ø  对象间关联不容易过期,即使过期了也只是更新一个id就行了,因为缓存中不是直接保存parent变量对应的父Person实例,而只是其id。 ![](https://box.kancloud.cn/2016-08-31_57c6b13a465b3.jpg) 不开启缓存和不开启关联缓存时,通过id加载操作对应的底层SQL执行情况如下。可以看出,如果不开启关联缓存的话,执行的SQL与不开启缓存时几乎一样(只省掉了查询父Person的第一条SQL): ![](https://box.kancloud.cn/2016-08-31_57c6b13a58b5b.jpg) 现在开启关联缓存来看一下其效果。开启关联缓存后,Person的缓存格式如下。缓存内容多了子Person对象的id集合,与父Person缓存类似,也是只保存id而不是所有子Person实例。至此,**Person对象本身以及嵌套的parent(Person)和children(Set<Person>)都被完全脱水成属性和关联id**: ![](https://box.kancloud.cn/2016-08-31_57c6b13a72536.jpg) 此时,**对Person对象的通过id加载操作完全不需要访问数据库了**,所以一定要开启关联缓存才能真正发挥出二级缓存的效果。但是,当执行HQL或SQL查询时,依然不会使用二级缓存。例如,我们通过firstName进行HQL查询时,Hibernate会先执行一条具有相同where条件的SQL获取出Person的id,有了id之后才能开始使用二级缓存中的内容,也就是说:**二级缓存只能在通过id查询时有用,对于其他复杂的条件查询是失效的**。而这条**查询出id的相同where条件的SQL就是查询缓存能发挥用处的地方**,于是就引出了查询缓存。 ![](https://box.kancloud.cn/2016-08-31_57c6b13a8dd40.jpg) ### 3.2 查询缓存 首先要配置开启查询缓存 <property name="hibernate.cache.use_query_cache">true</property> 此外还要在使用Query对象前调用setCacheable(true)。 查询缓存也是key-value的形式,其key是HQL或SQL以及参数,而value是查询结果的id集合,所以其value与开启关联缓存后的二级缓存非常像。所以,**只有在HQL或SQL一致,并且查询参数也完全一致的情况下,查询缓存才会有用!**现在来看一下两种缓存共存时的效果。当二级缓存的关联缓存和查询缓存都开启时,**当我们执行一个特定参数的HQL/SQL,首先会去查询缓存中找到对应的id,再去二级缓存中找到这些对象,以及这些对象关联的对象集合**。 ![](https://box.kancloud.cn/2016-08-31_57c6b13aa1ea4.jpg) ### 3.3 总结 不论是二级缓存还是查询缓存,本质上都是key-value形式保存的,所以**只能对key(id或HQL/SQL+参数)进行匹配查询。从这点上看,Hibernate缓存不够灵活,不能像IMDG产品那样,真正的将对象缓存在内存中,并提供面向对象的查询**。理解了Hibernate缓存的底层工作原理,才能更好的管理我们的缓存数据,理解Hibernate缓存的行为。 ### 参考资料 1 《The Definitive Guide to Terracotta》 2 [Hibernate: Truly Understanding the Second-Level and Query Caches](http://www.javalobby.org/java/forums/t48846.html)