hibernate 缓存
简单介绍缓存
缓存是广泛使用的用于优化数据库应用程序。缓存的目的是降低你的应用程序。并通过保存从数据库已载入数据的数据库之间的流量。
检索数据当前未在快速缓存仅当数据库訪问是必要的。应用程序可能须要从时间空(无效)的快速缓存,以时间。假设该数据库被更新或以某种方式改动,由于它无法知道缓存是否是最新的方式。
Hibernate的缓存
Hibernate使用的对象两个不同的缓存:一级缓存和二级缓存。第一级缓存与会话对象关联,而二级缓存与会话工厂对象关联。
默认情况下。Hibernate使用一级缓存,在每一个交易的基础。
Hibernate使用此缓存主要是降低数量的SQL查询,它须要一个给定的事务中产生。比如,假设一个对象在同一事务中多次改动,Hibernate将在交易结束时生成唯一一个SQL UPDATE语句。包含全部的改动。
本文重点介绍二级缓存。为了降低数据库流量,二级缓存保持在交易之间的会话工厂一级载入的对象。这些对象可用于整个应用程序,而不是只在执行查询的用户。这样,每一次一个查询返回已经载入在缓存一个对象。一个或多个数据库的交易可能被避免。
Cohesity数据平台:与融合的二级存储下载数据的效率如今此外,你能够,假设你须要缓存的实际查询结果,而不不过持久对象使用查询级缓存。
缓存实现
缓存是软件的复杂件,和市场提供了相当多的选择,既开源和商业。 Hibernate支持下面开源缓存实现外装即用:的EHCache(org.hibernate.cache.EhCacheProvider)
OSCache的(org.hibernate.cache.OSCacheProvider)
SwarmCache(org.hibernate.cache.SwarmCacheProvider)
JBoss TreeCache的(org.hibernate.cache.TreeCacheProvider)
每一个快速缓存提供了在性能方面。内存使用和配置的可能性不同容量:
的EHCache是一种快速,重量轻,易于使用过程中的快速缓存。它支持仅仅读和读/写快速缓存以及内存和基于磁盘的缓存。
然而,它不支持群集。
OSCache的是还有一个开源的缓存解决方式。这是一个较大的封装,这也为JSP页面或随意对象缓存功能的一部分。它是一个强大而灵活的包装,当中,像的EHCache,支持仅仅读和读/写快速缓存以及内存和基于磁盘的缓存。它还提供了通过不论什么JavaGroups或JMS集群的基本支持。
SwarmCache是一种基于JavaGroups一个简单的基于集群的缓存解决方式。
它支持仅仅读或不严格的读/写缓存(下一节解释这个词)。这样的类型的缓存是适当的,通常比写操作很多其它的读取操作的应用程序。
JBoss TreeCache的是一个强大的复制(同步或异步)和事务缓存。假设你真的须要一个真正的交易能力的缓存架构使用此解决方式。
还有一个缓存实现值得一提的是商业Tangosol的Coherence缓存。
快速缓存策略
一旦你选择了你的缓存实现,须要指定您的訪问策略。下面四种缓存策略可供选择:
仅仅读:这样的策略对于那些被频繁读取,但从来没有更新的数据。这是眼下为止最简单,性能最好的缓存策略。
读/写:假设须要更新您的数据的读/写缓存可能是合适的。他们携带比仅仅读缓存很多其它的开销。在非JTA的环境中,在Session.close()。或Session.disconnect()被调用的每一个事务都应完毕。
不严格的读/写:这样的策略不保证两个事务不会同一时候改动同样的数据。
因此,它可能是最适合于通常被读取但仅仅能偶尔改动的数据。
事务:这是可能仅仅在JTA环境中使用的全事务的缓存。
这些战略的支持是不是每一个缓存实现同样。表1显示了可用于不同的缓存实现的选项。
见图:
二级缓存
hibernate2.1版本号,基本原理和3.0、3.1是一样的
---------------------------------------------------------------
hibernate的session提供了一级缓存,每一个session,对同一个id进行两次load,不会发送两条sql给数据库。可是session关闭的时候,一级缓存就失效了。
二级缓存是SessionFactory级别的全局缓存。它底下能够使用不同的缓存类库,比方ehcache、oscache等,须要设置hibernate.cache.provider_class。我们这里用ehcache,在2.1中就是
hibernate.cache.provider_class=net.sf.hibernate.cache.EhCacheProvider
假设使用查询缓存。加上
hibernate.cache.use_query_cache=true
缓存能够简单的看成一个Map。通过key在缓存里面找value。
SessionFactory的内置缓存中存放了映射元数据和提前定义SQL语句。映射元数据是映射文件里数据的副本。而提前定义SQL语句是在 Hibernate初始化阶段依据映射元数据推导出来的。
SessionFactory的内置缓存是仅仅读的。应用程序不能改动缓存中的映射元数据和提前定义 SQL语句,因此SessionFactory不须要进行内置缓存与映射文件的同步。
SessionFactory的外置缓存是一个可配置的插件。在默认情况下,SessionFactory不会启用这个插件。外置缓存的数据是数据库数据 的副本。外置缓存的介质能够是内存或者硬盘。SessionFactory的外置缓存也被称为Hibernate的二级缓存。
Hibernate的二级缓存的实现原理与一级缓存是一样的,也是通过以ID为key的Map来实现对对象的缓存。
因为Hibernate的二级缓存是作用在SessionFactory范围内的,因而它比一级缓存的范围更广,能够被全部的Session对象所共享。
二级缓存的工作内容
Hibernate的二级缓存同一级缓存一样,也是针对对象ID来进行缓存。所以说。二级缓存的作用范围是针对依据ID获得对象的查询。
二级缓存的工作能够概括为下面几个部分:
● 在运行各种条件查询时,假设所获得的结果集为实体对象的集合。那么就会把全部的数据对象依据ID放入到二级缓存中。
● 当Hibernate依据ID訪问数据对象的时候。首先会从Session一级缓存中查找。假设查不到而且配置了二级缓存,那么会从二级缓存中查找。假设还查不到。就会查询数据库,把结果依照ID放入到缓存中。
● 删除、更新、添加数据的时候,同一时候更新缓存。
二级缓存的适用范围
Hibernate的二级缓存作为一个可插入的组件在使用的时候也是能够进行配置的。但并非全部的对象都适合放在二级缓存中。
在通常情况下会将具有下面特征的数据放入到二级缓存中:
● 非常少被改动的数据。
● 不是非常重要的数据,同意出现偶尔并发的数据。
● 不会被并发訪问的数据。
● 參考数据。
而对于具有下面特征的数据则不适合放在二级缓存中:
● 常常被改动的数据。
● 財务数据。绝对不同意出现并发。
● 与其它应用共享的数据。
在这里特别要注意的是对放入缓存中的数据不能有第三方的应用对数据进行更改(当中也包含在自己程序中使用其它方式进行数据的改动,比如。JDBC),由于那样Hibernate将不会知道数据已经被改动,也就无法保证缓存中的数据与数据库中数据的一致性。
二级缓存组件
在默认情况下,Hibernate会使用EHCache作为二级缓存组件。
可是,能够通过设置 hibernate.cache.provider_class属性,指定其它的缓存策略,该缓存策略必须实现 org.hibernate.cache.CacheProvider接口。
通过实现org.hibernate.cache.CacheProvider接口能够提供对不同二级缓存组件的支持。
Hibernate内置支持的二级缓存组件如表14.1所看到的。
组件 |
Provider类 |
类型 |
集群 |
查询缓存 |
Hashtable |
org.hibernate.cache.HashtableCacheProvider |
内存 |
不支持 |
支持 |
EHCache |
org.hibernate.cache.EhCacheProvider |
内存,硬盘 |
最新支持 |
支持 |
OSCache |
org.hibernate.cache.OSCacheProvider |
内存,硬盘 |
不支持 |
支持 |
SwarmCache |
org.hibernate.cache.SwarmCacheProvider |
集群 |
支持 |
不支持 |
JBoss TreeCache |
org.hibernate.cache.TreeCacheProvider |
集群 |
支持 |
支持 |
Hibernate已经不再提供对JCS(Java Caching System)组件的支持了。
Class的缓存
对于一条记录,也就是一个PO来说,是依据ID来找的,缓存的key就是ID。value是POJO。不管list。load还是iterate,仅仅要读出一个对象,都会填充缓存。
可是list不会使用缓存,而iterate会先取数据库select id出来,然后一个id一个id的load。如果在缓存里面有,就从缓存取,没有的话就去数据库load。如果是读写缓存,须要设置:
<cache usage="read-write"/>
假设你使用的二级缓存实现是ehcache的话,须要配置ehcache.xml
<cache name="com.xxx.pojo.Foo" maxElementsInMemory="500" eternal="false" timeToLiveSeconds="7200" timeToIdleSeconds="3600" overflowToDisk="true" />
当中eternal表示缓存是不是永远不超时,timeToLiveSeconds是缓存中每一个元素(这里也就是一个POJO)的超时时间,假设eternal="false",超过指定的时间,这个元素就被移走了。
timeToIdleSeconds是发呆时间,是可选的。
当往缓存里面put的元素超过500个时,假设overflowToDisk="true",就会把缓存中的部分数据保存在硬盘上的暂时文件中面。
每一个须要缓存的class都要这样配置。
假设你没有配置。hibernate会在启动的时候警告你,然后使用defaultCache的配置,这样多个class会共享一个配置。
当某个ID通过hibernate改动时,hibernate会知道,于是移除缓存。
这样大家可能会想,相同的查询条件。第一次先list,第二次再iterate,就能够使用到缓存了。实际上这是非常难的,由于你无法推断什么时候是第一次,并且每次查询的条件一般是不一样的。假如数据库里面有100条记录,id从1到100,第一次list的时候出了前50个id,第二次iterate的时候却查询到30至70号id,那么30-50是从缓存里面取的,51到70是从数据库取的,共发送1+20条sql。
所以我一直觉得iterate没有什么用,总是会有1+N的问题。
(题外话:有说法说大型查询用list会把整个结果集装入内存,非常慢。而iterate仅仅select id比較好,可是大型查询总是要分页查的。谁也不会真的把整个结果集装进来,假如一页20条的话,iterate共须要运行21条语句,list尽管选择若干字段,比iterate第一条select id语句慢一些,但仅仅有一条语句。不装入整个结果集hibernate还会依据数据库方言做优化。比方使用mysql的limit,总体看来应该还是list快。
)
假设想要对list或者iterate查询的结果缓存,就要用到查询缓存了
查询缓存
首先须要配置hibernate.cache.use_query_cache=true假设用ehcache,配置ehcache.xml。注意hibernate3.0以后不是net.sf的包名了
<cache name="net.sf.hibernate.cache.StandardQueryCache" maxElementsInMemory="50" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true"/> <cache name="net.sf.hibernate.cache.UpdateTimestampsCache" maxElementsInMemory="5000" eternal="true" overflowToDisk="true"/>然后
query.setCacheable(true);//激活查询缓存
query.setCacheRegion("myCacheRegion");//指定要使用的cacheRegion。可选
第二行指定要使用的cacheRegion是myCacheRegion。即你能够给每一个查询缓存做一个单独的配置。使用setCacheRegion来做这个指定。须要在ehcache.xml里面配置它:
<cache name="myCacheRegion" maxElementsInMemory="10" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true" />
假设省略第二行,不设置cacheRegion的话,那么会使用上面提到的标准查询缓存的配置,也就是net.sf.hibernate.cache.StandardQueryCache
对于查询缓存来说。缓存的key是依据hql生成的sql。再加上參数,分页等信息(能够通过日志输出看到。只是它的输出不是非常可读。最好改一下它的代码)。
比方hql:
from Cat c where c.name like ?
生成大致例如以下的sql:
select * from cat c where c.name like ?
參数是"tiger%",那么查询缓存的key*大约*是这种字符串(我是凭记忆写的,并不精确。只是看了也该明确了):
select * from cat c where c.name like ? , parameter:tiger%
这样,保证了相同的查询、相同的參数等条件下具有一样的key。
如今说说缓存的value。假设是list方式的话,value在这里并非整个结果集,而是查询出来的这一串ID。
也就是说。无论是list方法还是iterate方法,第一次查询的时候,它们的查询方式非常它们平时的方式是一样的。list运行一条sql。iterate运行1+N条。多出来的行为是它们填充了缓存。
可是到相同条件第二次查询的时候,就都和iterate的行为一样了。依据缓存的key去缓存里面查到了value。value是一串id,然后在到class的缓存里面去一个一个的load出来。
这样做是为了节约内存。
能够看出来,查询缓存须要打开相关类的class缓存。list和iterate方法第一次运行的时候,都是既填充查询缓存又填充class缓存的。
这里另一个非常easy被忽视的重要问题,即打开查询缓存以后,即使是list方法也可能遇到1+N的问题!同样条件第一次list的时候,由于查询缓存中找不到。无论class缓存是否存在数据,总是发送一条sql语句到数据库获取所有数据,然后填充查询缓存和class缓存。可是第二次运行的时候,问题就来了。假设你的class缓存的超时时间比較短,如今class缓存都超时了,可是查询缓存还在。那么list方法在获取id串以后。将会一个一个去数据库load!因此,class缓存的超时时间一定不能短于查询缓存设置的超时时间!假设还设置了发呆时间的话,保证class缓存的发呆时间也大于查询的缓存的生存时间。
这里还有其它情况,比方class缓存被程序强制evict了,这样的情况就请自己注意了。
另外。假设hql查询包括select字句。那么查询缓存里面的value就是整个结果集了。
当hibernate更新数据库的时候,它怎么知道更新哪些查询缓存呢?
hibernate在一个地方维护每一个表的最后更新时间,事实上也就是放在上面net.sf.hibernate.cache.UpdateTimestampsCache所指定的缓存配置里面。
当通过hibernate更新的时候,hibernate会知道这次更新影响了哪些表。然后它更新这些表的最后更新时间。每一个缓存都有一个生成时间和这个缓存所查询的表。当hibernate查询一个缓存是否存在的时候,假设缓存存在,它还要取出缓存的生成时间和这个缓存所查询的表,然后去查找这些表的最后更新时间,假设有一个表在生成时间后更新过了。那么这个缓存是无效的。
能够看出,仅仅要更新过一个表,那么凡是涉及到这个表的查询缓存就失效了。因此查询缓存的命中率可能会比較低。
Collection缓存
须要在hbm的collection里面设置<cache usage="read-write"/>
假如class是Cat。collection叫children,那么ehcache里面配置
<cache name="com.xxx.pojo.Cat.children" maxElementsInMemory="20" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true" />Collection的缓存和前面查询缓存的list一样。也是仅仅保持一串id,但它不会由于这个表更新过就失效,一个collection缓存仅在这个collection里面的元素有增删时才失效。
这样有一个问题,假设你的collection是依据某个字段排序的,当当中一个元素更新了该字段时,导致顺序改变时,collection缓存里面的顺序没有做更新。
缓存策略
仅仅读缓存(read-only):没有什么好说的读/写缓存(read-write):程序可能要的更新数据
不严格的读/写缓存(nonstrict-read-write):须要更新数据,可是两个事务更新同一条记录的可能性非常小。性能比读写缓存好
事务缓存(transactional):缓存支持事务。发生异常的时候。缓存也可以回滚。仅仅支持jta环境,这个我没有怎么研究过
读写缓存和不严格读写缓存在实现上的差别在于,读写缓存更新缓存的时候会把缓存里面的数据换成一个锁,其它事务假设去取对应的缓存数据。发现被锁住了,然后就直接取数据库查询。
在hibernate2.1的ehcache实现中,假设锁住部分缓存的事务发生了异常,那么缓存会一直被锁住。直到60秒后超时。
不严格读写缓存不锁定缓存中的数据。
使用二级缓存的前置条件
你的hibernate程序对数据库有独占的写訪问权,其它的进程更新了数据库,hibernate是不可能知道的。你操作数据库必需直接通过hibernate,假设你调用存储过程,或者自己使用jdbc更新数据库,hibernate也是不知道的。hibernate3.0的大批量更新和删除是不更新二级缓存的,可是据说3.1已经攻克了这个问题。这个限制相当的棘手,有时候hibernate做批量更新、删除非常慢,可是你却不能自己写jdbc来优化,非常郁闷吧。
SessionFactory也提供了移除缓存的方法,你一定要自己写一些JDBC的话,能够调用这些方法移除缓存。这些方法是:
void evict(Class persistentClass) Evict all entries from the second-level cache. void evict(Class persistentClass, Serializable id) Evict an entry from the second-level cache. void evictCollection(String roleName) Evict all entries from the second-level cache. void evictCollection(String roleName, Serializable id) Evict an entry from the second-level cache. void evictQueries() Evict any query result sets cached in the default query cache region. void evictQueries(String cacheRegion) Evict any query result sets cached in the named query cache region.只是我不建议这样做。由于这样非常难维护。比方你如今用JDBC批量更新了某个表,有3个查询缓存会用到这个表。用evictQueries(String cacheRegion)移除了3个查询缓存,然后用evict(Class persistentClass)移除了class缓存。看上去好像完整了。只是哪天你加入了一个相关查询缓存。可能会忘记更新这里的移除代码。假设你的jdbc代码到处都是。在你加入一个查询缓存的时候。还知道其它什么地方也要做对应的修改吗?
idea:
不要想当然的以为缓存一定能提高性能,只在你可以驾驭它而且条件合适的情况下才是这种。hibernate的二级缓存限制还是比較多的,不方便用jdbc可能会大大的减少更新性能。在不了解原理的情况下乱用,可能会有1+N的问题。不当的使用还可能导致读出脏数据。
假设受不了hibernate的诸多限制,那么还是自己在应用程序的层面上做缓存吧。
在越高的层面上做缓存,效果就会越好。就好像虽然磁盘有缓存,数据库还是要实现自己的缓存。虽然数据库有缓存。咱们的应用程序还是要做缓存。
由于底层的缓存它并不知道高层要用这些数据干什么,仅仅能做的比較通用,而高层能够有针对性的实现缓存。所以在更高的级别上做缓存。效果也要好些吧。