作者:赵磊
博客:http://elf8848.iteye.com
2010年11月7号,立冬,星期天。北京外面风好大,躲在家里整理一下这篇文章,发出来与大家分享,对大家有帮助是我最高兴的事儿。
不要想当然的认为使用了Hibernate的二级缓存就一定能够提高应用程序的性能,仅仅在你能够驾驭它,并且条件适合的情况下才是这样的。
使用hibernate的二级缓存限制还是比较多的。在不了解原理的情况下,可能缓存中的数据频繁的被清空性能下降,可能会有1+N的问题,在批量insert,update数据时二级缓存会占用大量内存,就算不溢出也会花费长时间来GC,不了解缓存的锁可能会出现死锁、脏数据。
一、缓存应用的场景:
1、对于新闻,论坛,博客等互联网应用适合在前端做缓存,比如url做为key来缓存整个页面的内容。一条新闻a被如前所述的缓存起来了,在网站并发访问量大时,会大大提高网站的吞吐能力。好了现再须要编辑这条新闻,如何同步更新缓存呢?须要立即同步更新缓存吗?不须要,互联网应用允许用户在5-10分钟之后再看到更新之后的新闻,这是可以接受的。没有较高的时效性,允许延迟。这样我们设定缓存对象的最大生命时间为10分钟,一个被缓存的对象存活时间超过10分钟就被清理,当新的访问请求到来时,再从数据库中加载他,再次被缓存10分钟。Hibernate二级缓存不适合这个场景,这个场景对缓存的锁、事务没有要求,对高并发,高数据量有要求。
总结一下:被缓存的对象没有较高的时效性,允许对象更新后延迟(10分钟内)展示,允许(10分钟内)的数据不一致。
2、对于企业应用,要保证数据的一致性是第一位的,即使数据被修改,最终用户看到的数据与数据库中的数据要时时一致。适合在应用程序持久层上做缓存,Hibernate二级缓存就适合这个场景。
总结一下:这个场景对缓存的锁,事务要求是第一的。对高并发高数据量的要求是第二的,通过锁保证数据的一致性。Hibernate对数据库是独占的,修改给数据库的操作都通过他.
二、频繁更新的数据要不要被缓存:
网上有人说频繁更新的数据不适合使用缓存。这样说是不全面的,因为他少说了前提条件。
数据一致性:本文章的数据一致性是指缓存中的数据与数据库中的数据就保持一致,严格的一致。决对没有脏数据。
当你须要数据一致性,而又不能保存数据一致性时,频繁更新的数据就不可以被缓存。不缓存直接操作数据库,就一致了,没有不一致的问题了。
你的Hibernate对数据库不是独占的,有其它程序来修改数据库中的记录,这时Hibernate是不知道的,也会发生数据不一致.
当使用url或sql语句做为KEY来缓存时,一句select 语句查出n个对象,无法在缓存中精准的找到被修改的某一个对象,当修改一个对象时就不能在缓存中精准的找到他,为了保证数据一致性,就要清除缓存中所有的同类对象,使下次查询时无法命中缓存。而不清除,很有可能发生数据不一致。这相当于Hibernate的查询缓存。这时个不要使用缓存。
当你须要数据一致性,而又能保存数据一致性时,频繁更新的数据是可以被缓存的。这里我们使用缓存的“锁”机制来保证,你使用的Hibernate第三方缓存要支持“锁”,就是read-write模式。这是重点啊。第三方缓存锁的实现方法不同性能也不同,锁是缓存性能下降第一原因,一定要使用高性能的锁,这就要了解多款Hibernate第三方缓存.
因为经常被更新修改的对象,一定也更加经常的被查询,需要缓存他来提高应用程序的性能。如果执行修改sql时,同时锁住缓存中的这个对象并更新他,之后解锁是最理想的。Hibernate的二级缓存策略,是针对于ID查询的缓存策略,所以可以做到精准的找到缓存中的目标,加之“锁”的帮助,可实现数据一致性。
但限制也有比如使用HQL时就不能精准的找到缓存中的目标,只好清除缓存中所有的同类对象来保证数据的一致性(缓存中没数据就没一致性问题了)。
三、Hibernate二级缓存中的对象什么时候会被清理:
在read-write模式下:
我们有一个Order对象,是一个实体对象,对应数据库中order表中的一条记录,经过查询已有n个Order对象被放入二级缓存中。现在我们要修改order表中任意任x条记录,执行以下HQL:
template.bulkUpdate("update Order set owner = ? where id in (?,?,?)");
这时Hibernate会直接将二级缓存中的n个Order对象清除掉。 天啊,居然不是你想像的修改谁就同步更新二级缓存中的谁,而是清除了二级缓存中全部的Order类型的对象。为什么?这一切是为了保证“数据一致性”。你执行了HQL修改了order表中的x条记录,这x条是哪几条?如果sql是子查询:update Order set owner =? where id in(select id from *** ),谁知道你修改了order表中的哪几条记录,你自己都不知道,Hibernate更不知道了。所以为了保证二级缓存中的数据与order表中的数据一致,只能清除了二级缓存中全部的Order类型的对象。二级缓存频繁的载入与清除,这样缓存命中率就会下降。
试验:看到这里后,我很担心,这样命中率下降后,没有起到缓存的作用。今天特意做一个实验,看看被缓存的对象在被修改后会怎样。
环境:Hibernate3.4 , OsCache(usage="read-write"),JUnit 缓存状态:Hibernate二级缓存中已缓存了5个Order对象。 测试结果: 1 使用saveOrUpdate()方法更新一个实体对象a时,新的a对象被put到二级缓存中,同时写入数据库,二级缓存中的其它4个Order对象没有变化。 这时再查询这5个Order对象中的任意,是可以命中二级缓存的。 2 使用HQL "update Order set name = ? where id =?" 方法更新一个实体对象a时,所有Order对象被从二级缓存中清除,同时a对象被写入数据库。 这时再查询这5个Order对象中的任意,无法命中二级缓存,会去查数据库,查出来的对象又put进二级缓存。
Hibernate的二级缓存策略,是以ID做为key 的缓存策略,在删除、更新、增加数据的时候,同时更新缓存。 对于条件查询,条件修改,条件删除(一般是执行HQL)则起不到缓存的作用。条件修改,条件删除时(一般是执行HQL)会清空所有在缓存中的同类对象。 为此,Hibernate提供了针对条件查询的Query Cache,其实它并不好用。
关于是否命中,是使用Statistics类监测的(通过SessionFactory的getStatistics()方法得到)。
总结一下:如果你打算开启hibernate的二级缓存,在修改与删除时,就要使用session.update(),session.delete()方法按ID一条一条的操作,这样对二级缓存是最优的。 但循环中使用sesion.update(),session.delete()方法,会产生多条sql语句,原本使用一条HQL完成的工作,现在要执行多条,你担心Hibernate与数据库服务器的网络通信次数吗?其实这多条sql是使用JDBC的批处理一次发送到数据库服务器的,所以你不用担心。现在到了数据库服务器端,我们以oracle为例,oracle要执行多条sql,就要进行多次的“分析sql语句的正确性,并解析成oracle的原子操作,并制定执行计划”,你担心这“多次”分析会给oracle带来性能的影响吗?不用担心,请使用oracle的绑定参数,就是Hibernate中的?代替参数。
四、Hibernate二级缓存的并发策略你了解吗:
1 只读缓存 read only 不须要锁与事务,因为缓存自数据从数据库加载后就不会改变。
如果数据是只读的,例如引用数据,那么总是使用“read-only”策略,因为它是最简单、最高效的策略,也是集群安全的策略。是性能第一的策略 。
2 读写缓存 read write 对缓存的更新发生在数据库事务完成后。缓存需要支持锁。在一个事务中更新数据库,在这个事务成功完成后更新缓存,并释放锁。 锁只是一种特定的缓存值失效表述方式,在它获得新数据库值前阻止其他事务读写缓存。那些事务会转而直接读取数据库。缓存必须支持锁,事务支持则不是必须的。如果缓存是一个集群,“更新缓存”的调用会将新值推送给所有副本,这通常被称为“推(push)”更新策略。
如果你的数据是又读又写的,那么使用“read-write”策略。这通常是性能第三的策略,因为它要求有缓存锁,缓存集群中使用重量级的“推”更新策略。
3 非严格读写缓存 nonstrict read write 在一个事务中更新数据库,在这个事务完成前就清除缓存,为了安全起见,无论事务成功与否,在事务完成后再次清除缓存。既不需要支持缓存锁,也不需要支持事务。如果是缓存集群,“清除缓存”调用会让所有副本都失效,这通常被称为“拉(pull)”更新策略。
如果你的数据读很多或者很少有并发缓存访问和更新,那么可以使用“nonstrict-read-write”策略。感谢它的轻量级“拉”更新策略,它通常是性能第二好的策略。
4 事务缓存 transactional (一定要在JTA环境中)对缓存和数据库的更新被包装在同一个JTA事务中,这样缓存与数据库总是保持同步的。数据库和缓存都必须支持JTA。
除非你真的想将缓存更新和数据库更新放在一个JTA事务里,否则不要使用“transactional”策略,因为JTA需要漫长的两阶段提交处理,这导致它基本是性能最差的策略。
五、缓存锁的性能也要了解,知道加了锁后性能会下降: 为了保证数据的安全性,不发生脏数据,各个缓存通常使用锁来保证 在本地方式运行时,缓存最大的开销就是使用锁来在保证共享数据完整性。 在集群环境中,RPC调用,锁,是性能上大开销。
下面以JBoss Cache为例说一说锁:JBoss Cache1.* 和 2.* 时代,提供乐观锁,悲观锁,但是性能不高。 JBoss Cache3.0 MVCC锁方案性能很高。
悲观锁:这些锁的隔离级别和数据库实施的隔离级别相同,这种方案简单而且健壮,允许多用户同时读取数据。读操作阻塞写操作,悲观锁的读写是互斥的,无法同时进行的,写的性能不好。
乐观锁:这个方式则牵涉到数据版本,可以获得高度并发性。那些请求读取数据的用户不会因为并发数据库写入操作而受到阻塞。而且,乐观锁定方式还可以避免悲观锁定中有可能发生的死锁。但它仍然有两个主要的缺点:一是性能问题。因为不断的将结点的状态拷贝到每个并发线程所造成的内存和 CPU 开销是不容忽略的。二是尽管并发时允许了写操作,但是一旦发现数据的版本不对,事务提交时不可避免的还是会失败。也就是说,此时写事务虽然可以不受限制的进行大量处理和写操作,但是这样在事务结束的时候容易出现提交失败。
多版本并发控制(MVCC):在数据访问速度上较之前者也胜出百倍。MVCC 提供了非阻塞 (non-blocking) 读操作 ( 它并不会去阻塞 wirter threads) ,在避免死锁的同时也提供了更高级的并发机制。更棒的是,我们的 MVCC 实现甚至可以对 reader threads 完全不采用任何锁 ( 对于像缓存这样频繁读取的系统来说,意义太大了 ) ,
六、批量处理时请不要使用二级缓存当你执行大量的 添加与修改时,并且这个实体对象被配置为启用二级缓存,你考虑过二级缓存会怎么样吗?请看下面代码:
Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); for ( int i=0; i<100000; i++ ) { Customer customer = new Customer(.....); //如果你的 hibernate.cache.use_second_level_cache 是 true, 请在会话级别上关闭他 //向(任何一级)缓存中加载大量数据通常也意味着它们很快会被清除出去,这会增加GC开销。 session.setCacheMode(CacheMode.IGNORE); session.save(customer); if ( i % 50 == 0 ) { //将本批插入的对象立即写入数据库并释放内存 session.flush(); session.clear(); } } tx.commit(); session.close();
批处理通常不需要数据缓存,否则你会将内存耗尽并大量增加GC开销。如果内存有限,那这种情况会很明显。
七、了解几种优秀缓存方案:
1、Memcached
分布式缓存系统,memcached 要求set的对象必须是可序列化对象,jboss cache等java obect cache是没有这个说法的,这是本质的不同的,但是他可以在网络上用,所以必须序列化也可理解。
独立服务器+java 客户端。
Memcached java 客户端有:
memcache-client-forjava,
XMemcached,
spymemcached,
memcache-client-forjava
参考文章:
XMemcached——一个新的开源Java memcached客户端
缓存系统MemCached的Java客户端优化历程
2、JBOSS CACHE
JBoss Cache是非常优秀的,前面介绍锁的时候已说过了。
参考文章:
深入理解JBoss Cache3.0——Naga
JBoss Cache分布式缓存:Manik Surtani访谈
3、EhCache
Ehcache 2.1起提供了针对Hibernate的JTA支持。
参考: Ehcache 2.0:后写式缓存和JTA支持
4、Infinispan
开源数据网格平台 ,是新东东。
参考: 开源数据网格平台Infinispan访谈