关于hibernate缓存的问题 基本的缓存原理
Hibernate缓存分为二级
第一级存放于session中称为一级缓存默认带有且不能卸载
第二级是由sessionFactory控制的进程级缓存是全局共享的缓存凡是会调用二级缓存的查询方法 都会从中受益只有经正确的配置后二级缓存才会发挥作用同时在进行条件查询时必须使用相应的方法才能从缓存中获取数据比如erate()方法loadget方法等必须注意的是sessionfind方法永远是从数据库中获取数据不会从二级缓存中获取数据即便其中有其所需要的数据也是如此
查询时使用缓存的实现过程为首先查询一级缓存中是否具有需要的数据如果没有查询二级缓存如果二级缓存中也没有此时再执行查询数据库的工作要注意的是此种方式的查询速度是依次降低的
存在的问题
一级缓存的问题以及使用二级缓存的原因
因为Session的生命期往往很短存在于Session内部的第一级最快缓存的生命期当然也很短所以第一级缓存的命中率是很低的其对系统性能的改善也是很有限的当然这个Session内部缓存的主要作用是保持Session内部数据状态同步并非是hibernate为了大幅提高系统性能所提供的
为了提高使用hibernate的性能除了常规的一些需要注意的方法比如
使用延迟加载迫切外连接查询过滤等以外还需要配置hibernate的二级缓存其对系统整体性能的改善往往具有立竿见影的效果!
(经过自己以前作项目的经验一般会有~倍的性能提高)
N+次查询的问题
什么时候会遇到+N的问题?
前提Hibernate默认表与表的关联方法是fetch=select不是fetch=join这都是为了懒加载而准备的
)一对多(<set><list>) 在的这方通过条sql查找得到了个对象由于关联的存在 那么又需要将这个对象关联的集合取出所以合集数量是n还要发出n条sql于是本来的条sql查询变成了 +n条
)多对一<manytoone> 在多的这方通过条sql查询得到了n个对象由于关联的存在也会将这n个对象对应的 方的对象取出 于是本来的条sql查询变成了 +n条
)iterator 查询时一定先去缓存中找(条sql查集合只查出ID)在没命中时会再按ID到库中逐一查找 产生+n条SQL
怎么解决+N 问题?
)lazy=true hibernate开始已经默认是lazy=true了lazy=true时不会立刻查询关联对象只有当需要关联对象(访问其属性非id字段)时才会发生查询动作
)使用二级缓存 二级缓存的应用将不怕+N 问题因为即使第一次查询很慢(未命中)以后查询直接缓存命中也是很快的刚好又利用了+N
) 当然你也可以设定fetch=join一次关联表全查出来但失去了懒加载的特性
执行条件查询时iterate()方法具有着名的 n+次查询的问题也就是说在第一次查询时iterate方法会执行满足条件的查询结果数再加一次(n+)的查询但是此问题只存在于第一次查询时在后面执行相同查询时性能会得到极大的改善此方法适合于查询数据量较大的业务数据
但是注意当数据量特别大时(比如流水线数据等)需要针对此持久化对象配置其具体的缓存策略比如设置其存在于缓存中的最大记录数缓存存在的时间等参数以避免系统将大量的数据同时装载入内存中引起内存资源的迅速耗尽反而降低系统的性能!!!
使用hibernate二级缓存的其他注意事项
关于数据的有效性
另外hibernate会自行维护二级缓存中的数据以保证缓存中的数据和数据库中的真实数据的一致性!无论何时当你调用save()update()或 saveOrUpdate()方法传递一个对象时或使用load() get()list()iterate() 或scroll()方法获得一个对象时 该对象都将被加入到Session的内部缓存中 当随后flush()方法被调用时对象的状态会和数据库取得同步
也就是说删除更新增加数据的时候同时更新缓存当然这也包括二级缓存!
只要是调用hibernate API执行数据库相关的工作hibernate都会为你自动保证 缓存数据的有效性!!
但是如果你使用了JDBC绕过hibernate直接执行对数据库的操作此时Hibernate不会/也不可能自行感知到数据库被进行的变化改动也就不能再保证缓存中数据的有效性!!
这也是所有的ORM产品共同具有的问题幸运的是Hibernate为我们暴露了Cache的清除方法这给我们提供了一个手动保证数据有效性的机会!!
一级缓存二级缓存都有相应的清除方法
其中二级缓存提供的清除方法为
按对象class清空缓存
按对象class和对象的主键id清空缓存
清空对象的集合中的缓存数据等
适合使用的情况
并非所有的情况都适合于使用二级缓存需要根据具体情况来决定同时可以针对某一个持久化对象配置其具体的缓存策略
适合于使用二级缓存的情况
数据不会被第三方修改
一般情况下会被hibernate以外修改的数据最好不要配置二级缓存以免引起不一致的数据但是如果此数据因为性能的原因需要被缓存同时又有可能被第方比如SQL修改也可以为其配置二级缓存只是此时需要在sql执行修改后手动调用cache的清除方法以保证数据的一致性
数据大小在可接收范围之内
如果数据表数据量特别巨大此时不适合于二级缓存原因是缓存的数据量过大可能会引起内存资源紧张反而降低性能
如果数据表数据量特别巨大但是经常使用的往往只是较新的那部分数据此时也可为其配置二级缓存但是必须单独配置其持久化类的缓存策略比如最大缓存数缓存过期时间等将这些参数降低至一个合理的范围(太高会引起内存资源紧张太低了缓存的意义不大)
数据更新频率低
对于数据更新频率过高的数据频繁同步缓存中数据的代价可能和 查询缓存中的数据从中获得的好处相当坏处益处相抵消此时缓存的意义也不大
非关键数据(不是财务数据等)
财务数据等是非常重要的数据绝对不允许出现或使用无效的数据所以此时为了安全起见最好不要使用二级缓存
因为此时 正确性的重要性远远大于 高性能的重要性
目前系统中使用hibernate缓存的建议
目前情况
一般系统中有三种情况会绕开hibernate执行数据库操作
多个应用系统同时访问一个数据库
此种情况使用hibernate二级缓存会不可避免的造成数据不一致的问题此时要进行详细的设计比如在设计上避免对同一数据表的同时的写入操作
使用数据库各种级别的锁定机制等
动态表相关
所谓动态表是指在系统运行时根据用户的操作系统自动建立的数据表
比如自定义表单等属于用户自定义扩展开发性质的功能模块因为此时数据表是运行时建立的所以不能进行hibernate的映射因此对它的操作只能是绕开hibernate的直接数据库JDBC操作
如果此时动态表中的数据没有设计缓存就不存在数据不一致的问题
如果此时自行设计了缓存机制则调用自己的缓存同步方法即可
使用sql对hibernate持久化对象表进行批量删除时
此时执行批量删除后缓存中会存在已被删除的数据
分析
当执行了第条(sql批量删除)后后续的查询只可能是以下三种方式
a sessionfind()方法
根据前面的总结find方法不会查询二级缓存的数据而是直接查询数据库
所以不存在数据有效性的问题
b 调用iterate方法执行条件查询时
根据iterate查询方法的执行方式其每次都会到数据库中查询满足条件的id值然后再根据此id 到缓存中获取数据当缓存中没有此id的数据才会执行数据库查询
如果此记录已被sql直接删除则iterate在执行id查询时不会将此id查询出来所以即便缓存中有此条记录也不会被客户获得也就不存在不一致的情况(此情况经过测试验证)
c 用get或load方法按id执行查询
客观上此时会查询得到已过期的数据但是又因为系统中执行sql批量删除一般是针对中间关联数据表对于中间关联表的查询一般都是采用条件查询 按id来查询某一条关联关系的几率很低所以此问题也不存在!
如果某个值对象确实需要按id查询一条关联关系同时又因为数据量大使用 了sql执行批量删除当满足此两个条件时为了保证按id 的查询得到正确的结果可以使用手动清楚二级缓存中此对象的数据的方法!!(此种情况出现的可能性较小)
建 议
建议不要使用sql直接执行数据持久化对象的数据的更新但是可以执行 批量删除(系统中需要批量更新的地方也较少)
如果必须使用sql执行数据的更新必须清空此对象的缓存数据调用
SessionFactoryevict(class)
SessionFactoryevict(classid)等方法
在批量删除数据量不大的时候可以直接采用hibernate的批量删除这样就不存在绕开hibernate执行sql产生的缓存数据一致性的问题
不推荐采用hibernate的批量删除方法来删除大批量的记录数据
原因是hibernate的批量删除会执行条查询语句外加 满足条件的n条删除语句而不是一次执行一条条件删除语句!!
当待删除的数据很多时会有很大的性能瓶颈!!!如果批量删除数据量较大比如超过条可以采用JDBC直接删除这样作的好处是只执行一条sql删除语句性能会有很大的改善同时缓存数据同步的问题可以采用 hibernate清除二级缓存中的相关数据的方法
调 用
SessionFactoryevict(class) ;
SessionFactoryevict(classid)等方法
所以说对于一般的应用系统开发而言(不涉及到集群分布式数据同步问题等)因为只在中间关联表执行批量删除时调用了sql执行同时中间关联表一般是执行条件查询不太可能执行按id查询所以此时可以直接执行sql删除甚至不需要调用缓存的清除方法这样做不会导致以后配置了二级缓存引起数据有效性的问题
退一步说即使以后真的调用了按id查询中间表对象的方法也可以通过调用清除缓存的方法来解决
具体的配置方法
根据我了解的很多hibernate的使用者在调用其相应方法时都迷信的相信hibernate会自行为我们处理性能的问题或者hibernate 会自动为我们的所有操作调用缓存实际的情况是hibernate虽然为我们提供了很好的缓存机制和扩展缓存框架的支持但是必须经过正确的调用其才有可能发挥作用!!所以造成很多使用hibernate的系统的性能问题实际上并不是hibernate不行或者不好而是因为使用者没有正确的了解其使用方法造成的相反如果配置得当hibernate的性能表现会让你有相当惊喜的发现下面我讲解具体的配置方法
ibernate提供了二级缓存的接口
netsfhibernatecacheProvider
同时提供了一个默认的 实现netsfhibernatecacheHashtableCacheProvider
也可以配置 其他的实现 比如ehcachejbosscache等
具体的配置位置位于hibernatecfgxml文件中
<propertyname=hibernatecacheuse_query_cache>true</property>
<propertyname=hibernatecacheprovider_class>netsfhibernatecacheHashtableCacheProvider</property>
很多的hibernate使用者在 配置到 这一步 就以为 完事了
注意其实光这样配根本就没有使用hibernate的二级缓存同时因为他们在使用hibernate时大多时候是马上关闭session所以一级缓存也没有起到任何作用结果就是没有使用任何缓存所有的hibernate操作都是直接操作的数据库!!性能可以想见
正确的办法是除了以上的配置外还应该配置每一个vo对象的具体缓存策略在影射文件中配置例如
<hibernatemapping>
<classname=comsobeysbmmodelentitySystemvoDataTypeVOtable=dcm_datatype>
<cacheusage=readwrite/>
<idname=idcolumn=TYPEIDtype=javalangLong>
<generatorclass=sequence/>
</id>
<propertyname=namecolumn=NAMEtype=javalangString/>
<propertyname=dbTypecolumn=DBTYPEtype=javalangString/>
</class>
</hibernatemapping>
关键就是这个<cache usage=readwrite/>其有几个选择readonlyreadwritetransactional等
然后在执行查询时 注意了 如果是条件查询或者返回所有结果的查询此时sessionfind()方法 不会获取缓存中的数据只有调用erate()方法时才会调缓存的数据
同时 get 和 load方法 是都会查询缓存中的数据
对于不同的缓存框架具体的配置方法会有不同但是大体是以上的配置(另外对于支持事务型以及支持集群的环境的配置我会争取在后续的文章中中 发表出来)