缓存在应用程序中的使用十分广泛,由于缓存的介质一般是内存,读写速度快,所以通常我们会把常用的或者要通过比较耗时或大量资源取得的数据缓存起来,从而加整后续的使用。这样在缓存的应用过程中,就需要考虑缓存并发访问的管理以及缓存数据的生命周期。
通常,缓存的范围决定了缓存的生命周期以及可以被谁访问。缓存的范围可分为三类。
1 事务范围:缓存只能被当前事务访问。缓存的生命周期依赖于事务的生命周期,当事务结束时,缓存也就结束生命周期。在此范围下,缓存的介质是内存。事务可以是数据库事务或者应用事务,每个事务都有独自的缓存,缓存内的数据通常采用相互关联的的对象形式。
2 进程范围:缓存被进程内的所有事务共享。这些事务有可能是并发访问缓存,因此必须对缓存采取必要的事务隔离机制。缓存的生命周期依赖于进程的生命周期,进程结束时,缓存也就结束了生命周期。进程范围的缓存可能会存放大量的数据,所以存放的介质可以是内存或硬盘。缓存内的数据既可以是相互关联的对象形式也可以是对象的松散数据形式。松散的对象数据形式有点类似于对象的序列化数据,但是对象分解为松散的算法比对象序列化的算法要求更快。
3 集群范围:在集群环境中,缓存被一个机器或者多个机器的进程共享。缓存中的数据被复制到集群环境中的每个进程节点,进程间通过远程通信来保证缓存中的数据的一致性,缓存中的数据通常采用对象的松散数据形式。
在Hibernate中提供了两级缓存:
Session级缓存(一级缓存或事务级缓存),是Hibernate内置并不能被卸载的缓存,生命周期和session一致,属于事务范围的缓存,只缓存实体。应用不能对Session级缓存进行管理,所以不需要显式调用。
SessionFactory级缓存(二级缓存或进程级缓存),使用第3方插件实现,生命周期sessionFactory一致,应用可以对其进行管理。SessionFactory级缓存被SessionFactory创建的所有Session实例所共享,所以可以跨session访问。第二级缓存是可选的,可以在每个类或每个集合的粒度上配置第二级缓存。
此外还有一个与二级缓存一起使用的查询缓存,它主要对查询结果集进行缓存,对实体对象的结果集会缓存实体id。它有两个缓存区域:一个用于保存查询结果集(org.hibernate.cache.StandardQueryCache); 另一个则用于保存最近查询的一系列表的时间戳(org.hibernate.cache.UpdateTimestampsCache)。当关联的表发生修改,查询缓存的生命周期结束。通常只有当经常使用同样的参数进行查询时,才会有用处,所以多数情况下是用不上查询缓存的。
查询缓存的配置和使用:
> 启用查询缓存,修改hibernate.cfg.xml文件
<property name="hibernate.cache.use_query_cache">true</property>
> 在程序中启用,如:
query.setCacheable(true);
在应用中,因为Session的生命期往往都比较短,session级缓存的生命期和session一致,这样导致应用在第一级缓存的命中率实际并不高。所以Session级缓存并不能大幅提高系统性能。在Hibernate中, Session级缓存的主要作用是保持Session内部数据状态同步。为提高应用系统的性能,需要引入二级缓存。由于二级缓存被进程内所有事务共享,所以可能出现并发问题,因此需要采用适当的缓存同步策略,该策略为被缓存的数据提供事务隔离级别。
一级缓存和二级缓存的比较
| 第一级缓存 | 第二级缓存 |
存放数据的形式 | 相互关联的持久化对象 | 对象的散装数据 |
缓存的范围 | 事务范围,每个事务都有单独的第一级缓存 | 进程范围或集群范围,缓存被同一个进程或集群范围内的所有事务共享 |
并发访问策略 | 由于每个事务都拥有单独的第一级缓存,不会出现并发问题,无需提供并发访问策略 | 由于多个事务会同时访问第二级缓存中相同数据,因此必须提供适当的并发访问策略,来保证特定的事务隔离级别 |
数据过期策略 | 没有提供数据过期策略。处于一级缓存中的对象永远不会过期,除非应用程序显式清空缓存或者清除特定的对象 | 必须提供数据过期策略,如基于内存的缓存中的对象的最大数目,允许对象处于缓存中的最长时间,以及允许对象处于缓存中的最长空闲时间 |
物理存储介质 | 内存 | 内存和硬盘。对象的散装数据首先存放在基于内在的缓存中,当内存中对象的数目达到数据过期策略中指定上限时,就会把其余的对象写入基于硬盘的缓存中。 |
缓存的软件实现 | 在Hibernate的Session的实现中包含了缓存的实现 | 由第三方提供,Hibernate仅提供了缓存适配器(CacheProvider)。用于把特定的缓存插件集成到Hibernate中。 |
启用缓存的方式 | 只要应用程序通过Session接口来执行保存、更新、删除、加载和查询数据库数据的操作,Hibernate就会启用第一级缓存,把数据库中的数据以对象的形式拷贝到缓存中,对于批量更新和批量删除操作,如果不希望启用第一级缓存,可以绕过Hibernate API,直接通过JDBC API来执行指操作。 | 用户可以在单个类或类的单个集合的粒度上配置第二级缓存。如果类的实例被经常读但很少被修改,就可以考虑使用第二级缓存。只有为某个类或集合配置了第二级缓存,Hibernate在运行时才会把它的实例加入到第二级缓存中。 |
用户管理缓存的方式 | 第一级缓存的物理介质为内存,由于内存容量有限,必须通过恰当的检索策略和检索方式来限制加载对象的数目。Session的evit()方法可以显式清空缓存中特定对象,但这种方法不值得推荐。 | 第二级缓存的物理介质可以是内存和硬盘,因此第二级缓存可以存放大量的数据,数据过期策略的maxElementsInMemory属性值可以控制内存中的对象数目。管理第二级缓存主要包括两个方面:选择需要使用第二级缓存的持久类,设置合适的并发访问策略:选择缓存适配器,设置合适的数据过期策略。 |
由于缓存与增删改查操作密切相关,参考手册中提到“无论何时,当你给save()、update()或 saveOrUpdate()方法传递一个对象时,或使用load()、 get()、list()、iterate() 或scroll()方法获得一个对象时, 该对象都将被加入到Session的内部缓存中。 当随后flush()方法被调用时,对象的状态会和数据库取得同步。” 也就是说调用方法save()、update()、saveOrUpdate()、或查询方法load()、get()、list()、iterate()或Scroll()时,会同时更新缓存。但在二级缓存的处理上有所不同:
● load()方法
在使用了二级缓存的情况下,调用load()方法,Hibernate首先从当前Session的一级缓存中获取ID对应的值,在获取不到的情况下,则从二级缓存中获取ID对应的值,如仍然获取不到则还需要根据是否配置了延迟加载来决定如何执行,如未配置延迟加载则从数据库中直接获取。在从数据库获取到数据的情况下,Hibernate会相应地填充一级缓存和二级缓存,如配置了延迟加载则直接返回一个代理类,只有在触发代理类的调用时才进行数据库的查询操作。
● get()方法
同样,在使用了二级缓存的情况下,调用get()方法,Hibernate会在当前Session的一级缓存中查询不到指定的对象时,直接执行SQL语句从数据库中取得所需的数据。
在使用二级缓存情况下,get()方法与load()方法的区别就在于:1)Load()方法会查询二级缓存,而get()方法不会。2)load支持延迟加载,get不支持延迟加载。
无论是get还是load,都会首先查找缓存(一级缓存)。调用clear()方法,可以强制清除session缓存,调用flush()方法可以强制进行从内存到数据库的同步。
● list()方法
在调用Query的list()方法时,先检查是否配置了查询缓存,如配置了则从查询缓存中寻找是否已经对该查询进行了缓存,如获取不到则从数据库中进行获取。从数据库中获取到后,Hibernate将会相应地填充一级、二级和查询缓存。如获取到的为直接的结果集,则直接返回,如获取到的为一些ID的值,则再根据ID获取相应的值(Session.load()),最后形成结果集返回。可以看到,在这样的情况下,list()方法也是有可能造成N次查询的。
查询缓存在数据发生任何变化的情况下都会被自动清空。
● iterator()方法
在调用Query的iterator()方法时,它首先会使用查询语句得到ID值的列表,然后再使用Session的load()方法得到所需要的对象的值。
在使用二级缓存情况下,list方法与iterator方法的区别在于:list每次都会发出查询语句,list向缓存中放入数据,但不利用缓存中的数据,而iterate会先从数据库中select id出来,然后再一个id一个id的加载,如果缓存里面有相应的对象,就从缓存取,没有再去数据库读取。
(query.list会使用查询缓存,而query.iterate不会使用查询缓存)
list不使用一级缓存 只会向 缓存中插入数据 所以list 每次都会发出sql语句。不管是list方法还是iterate方法,在第一次查询时,list会执行一条sql,iterate会执行1+N条,取得的数据会填充缓存和查询缓存(list和iterate方法第一次执行时,都是既填充查询缓存又填充class缓存)。在其后的查询中,会利用到第一次的缓存。
在开发过程中,获取数据的时候,应该依据这4种获取数据方式的特点来选择合适的方法。并可以通过设置show_sql选项来输出Hibernate所执行的SQL语句,以此来了解Hibernate是如何操作数据库的。
Hibernate会自动维护缓存中的数据。默认情况下,session会在下列执行点执行数据同步:
1) 当调用Transaction的commit()方法时,会先清理缓存,再执行提交事务
2) 当调用Query的list()或者iterator()时,如果缓存中持久化对象的属性发生了变化,就会先清理缓存,以保证查询结果能反映持久化对象的最新状态。
3) 当应用程序显式调用session的flush()方法时。
Session的setFlushMode()方法用于设置flush策略。FlushMode类定义了五种不同的状态同步模式:NEVER、AUTO、COMMIT、MANUAL和ALWAYS。其中NEVER用MANUAL代替了。例如,以下代码显式设置FlushModo.Commit:
session.setFlushMode(FlushMode.COMMIT);
Flush模式 | Session的查询方法 | Session的commit()方法 | Session的flush()方法 |
FlushMode.AUTO | flush | flush | Flush |
FlushMode.COMMIT | No | flush | flush |
FlushMode.MANUAL | No | No | No |
FlushMode.ALWAYS | flush | flush | flush |
ALWAYS与AUTO的区别:当hibernate缓存中的对象被改动之后会被标记为脏数据,当session设置为FlushMode.AUTO时,hibernate在进行查询的时候会判断缓存中的数据是否为脏数据,是则刷新数据库。而always则不判断直接进行刷新。
FlushMode.AUTO是默认值,应该优先考虑的模式,它会保证在整个事务中保持数据的一致。
我们还可以调用session.evict() 方法,从一级缓存中去掉数据对象及其集合。 如若要把所有的对象从session缓存中彻底清除,则需调用Session.clear()。
对于二级缓存来说,在SessionFactory中定义了许多方法, 清除缓存中实例、整个类、集合实例或者整个集合。
sessionFactory.evict(Cat.class, catId); //evict a particular Cat
sessionFactory.evict(Cat.class); //evict all Cats
sessionFactory.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens
sessionFactory.evictCollection("Cat.kittens"); //evict all kitten collections
CacheMode参数用于控制具体的Session如何与二级缓存进行交互。
CacheMode.NORMAL - 从二级缓存中读、写数据。
CacheMode.GET - 从二级缓存中读取数据,仅在数据更新时对二级缓存写数据。
CacheMode.PUT - 仅向二级缓存写数据,但不从二级缓存中读数据。
CacheMode.REFRESH - 仅向二级缓存写数据,但不从二级缓存中读数据。通过 hibernate.cache.use_minimal_puts的设置,强制二级缓存从数据库中读取数据,刷新缓存内容。
如若需要查看二级缓存或查询缓存区域的内容,你可以使用统计(Statistics) API。
Map cacheEntries = sessionFactory.getStatistics()
.getSecondLevelCacheStatistics(regionName)
.getEntries();
此时,你必须手工打开统计选项。可选的,你可以让Hibernate更人工可读的方式维护缓存内容。
hibernate.generate_statistics true
hibernate.cache.use_structured_entries true
批量增修改对缓存有较大的影响。我们知道一级缓存由Hibernate管理,并且容量没有限制。所以加载大批量数据时,会占用大量内存。二级缓存同理。所以在批量操作时,我们需要手动来清除缓存的对象,以控制内存的使用。
session.evict();//清除session缓存
SessionFactory.evict();//清除二级缓存
对二级缓存提供的清除方法主要为:按对象class清空缓存、按对象class和对象的主键id清空缓存、清空对象的集合中的缓存数据等。
由于二级缓存被进程内所有事务共享,可能会出现并发问题,因此我们需要考虑采用适当的缓存同步策略。缓存同步策略决定了数据对象在缓存中的存取规则。Hibernate提供了以下4种内置的缓存同步策略:
- Read-only: 只读,对于不会发生改变的数据,可使用只读型缓存。
- Nonstrict-read-write:如果程序对并发访问下的数据同步要求不是非常严格,且数据更新操作频率较低,可采用本选项,以获得较好性能。
- Read-write:严格可读写缓存。基于时间戳判定机制,实现了“read Committed”事务隔离等级。可用于对数据同步要求严格的情况,但不支持分布式缓存。
- Transactional:事务型缓存,必须运行在JTA事务环境中。
在事务型缓存中,缓存相关操作也被添加到事务中(此时的缓存,类似一个内存数据库),如果由于某种原因导致事务失败,可以连同缓冲池中的数据一同回滚到事务开始之前的状态。事务型缓存实现了“Repeatable read”事务隔离等级,有效保障了数据的合法性,适用于对关键数据的缓存。
我们在选择第三方缓存时,应该注意其支持的缓存并发策略。
我们知道大部分缓存实现就是一个key-value的Map存储器,如何运用好缓存帮忙我们的应用提升性能,不管是自行设计的缓存还是利用Hibernate支持的二级缓存,都需要考虑到缓存Key键的设计、缓存数据的更新机制、需要缓存的数据筛选和缓存布署以及在架构中的位置或地位。我们知道如果缓存中的数据频繁更新会带来缓存数据命中率的下降,使缓存失去应有的作用。所以缓存中的数据是常被查询且更新缓慢的对象。而应用中对象的增删改操作必然会影响到缓存数据,特别是数据的批量更新和删除操作。在批量更新或删除时,最简单的方式就是清空缓存数据,然后在随后的操作中重建缓存。这种粗暴的方式在于简单可靠,但并不能适应大多数的应用场景。我们常把Hibernate与Spring结合使用,在Spring的缓存管理中再总结一下。
除了二级缓存,我们还看到Hibernate自身的一级缓存的管理。出于维护一级缓存,Hibernate在数据保存过程会有一个脏数据的判定过程(数据状态的维护)。所以在大批量实时数据保存过程中,需要考虑Hibernate一级缓存和数据状态维护的影响。在实时数据处理中,Hibernate框架是否适合还需看需而定。
参考:Hibernate参考手册、深入浅出Hibernate、百度百科-Hibernate缓存机制