Cache

        缓存(Cache )是计算机领域非常通用的概念。它介于应用程序和永久性数据存储源(如硬盘上的文件或者内存)之间,其作用是降低应用程序直接读写永久性数据存储源的频率,从而提高应用的运行性能。缓存中的数据是数据存储源中数据的拷贝,应用程序在运行时直接读写缓存中的数据,只在某些特定时刻按照缓存中的数据来同步更新数据存储源(JPA中persist方法、merge方法是声明实体的状态,并没有将实体持久化到数据库中,通过自动或手动调用flush才会将数据同步到数据库)。
       缓存的物理介质通常是内存,而永久性数据存储源的物理介质通常是硬盘或磁盘,应用程序读写内存的速度显然比读写硬盘的速度快,如果缓存中存放的数据量非常大,也会用硬盘作为缓存的物理介质。

       需要被缓存的数据:经常被访问、很少被修改并且需要共享访问的数据。

 

一级缓存

       基于Hibernate的JPA一级缓存是由EntityManager提供的,因此它只存在于EntityManager的生命周期中,当程序调用persist、find、getResultList 等方法时,如EntityManager缓存中还不存在相应的对象,JPA会把该对象加入到一级缓存中,当EntityManager关闭的时候该EntityManager所管理的一级缓存也会立即被清除,一级缓存是EntityManager所内置的,不能被卸载,也不能进行任何配置。

 

二级缓存

       基于Hibernate的JPA二级缓存是EntityManagerFactory提供的,能够缓存不同EntityManager中的实体对象。EntityManager查询策略为:首先查询一级缓存(当前EntityManager)中的托管数据,如果找不到查询EntityManagerFactory中缓存的数据,如果仍找不到则进行数据库查询。

 

缓存方式

       read-only:只读模式,在此模式下,如果对数据进行更新操作,会有异常。

       read_write:读写模式在更新缓存的时候会把缓存里面的数据换成一个锁,其它事务如果去取相应的缓存数据,发现被锁了,直接就去数据库查询;如果应用程序需要更新数据,那么使用读/写缓存比较合适。如果应用程序要求“序列化事务”的隔离级别(serializable transaction isolation level),那么就决不能使用这种缓存策略。
       nonstrict read/write:如果应用程序只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离。不保证缓存与数据库中数据的一致性,如果存在两个事务同时访问缓存中相同数据的可能,必须为该数据配置一个很短的数据过期时间,从而尽量避免脏读。对于极少被修改,并且允许偶尔脏读的数据,可以采用这种并发访问策略。

      transactional:仅仅在事务环境中使用,它提供了Repeatable Read事务隔离级别。对于经常被读但很少修改的数据,可以采用这种隔离类型,因为它可以防止脏读和不可重复读这类的并发问题。

 

缓存配置

配置基于Hibernate的缓存信息



<!-- 产生缓存统计信息 -->
				<entry key="hibernate.generate_statistics" value="true"/>
				<!-- 使用二级缓存 -->
				<entry key="hibernate.cache.use_second_level_cache" value="true" />
            	<entry key="hibernate.cache.use_structured_entries" value ="true" />
            	<!-- 使用查询缓存 -->  
            	<entry key="hibernate.cache.use_query_cache" value="true" /> 
            	<!-- Hibernate4第三方cache --> 
				<entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
				<entry key="net.sf.ehcache.configurationResourceName" value="/ehcache-hibernate-local.xml"/>

hibernate.cache.use_second_level_cache:是否使用二级缓存。二级缓存只能够缓存实体对象,即只能够缓存使用find方法获取的实体对象。


hibernate.cache.use_query_cache:是否启用查询缓存。是针对普通属性结果集的缓存。(没有到验证use_query_cache=true或false时对缓存查询是否有影像。)

hibernate.cache.region.factory_class:第三方的缓存类。

net.sf.ehcache.configurationResourceName:encache查询缓存配置文件。

ehcache-hibernate-local.xml文件内容



<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="hibernateCache">
	<diskStore path="java.io.tmpdir/ehcache/jpa" />
	<!-- 使用encache2.6,否则提示不存在属性maxEntriesLocalHeap -->
	<defaultCache maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
		overflowToDisk="true" maxEntriesLocalDisk="100000" />
	<!-- 也可以单独为某个实体指定配置信息 -->
<!-- 	<cache name="com.projects.jpa.entity.Customer" maxEntriesLocalHeap="1000" eternal="true" -->
<!-- 		overflowToDisk="true" maxEntriesLocalDisk="10000"/> -->
</ehcache>



为需要缓存的实体类配置Hibernate的Cache属性,如果不配置Cache注解,是不对实体进行二级缓存的。



@Cache(usage=CacheConcurrencyStrategy.READ_WRITE) public class Customer extends BaseEntity { ...... }



问题:即使以上都配置了,使用getResultList也不能使用缓存,需要为Query对象明确指定使用缓存如下:

Query query = entityManager.createQuery("from Customer c") ;
		query.setHint("org.hibernate.cacheable", true);
		List<Customer> list = query.getResultList() ;

      

 

参考文章:http://www.iteye.com/topic/1133637

 

目前在使用spring-data-jpa和hibernate4的时候,对于缓存关系不是很清楚,以及二级缓存和查询缓存的各种配置等等,于是就有了这篇初级的jpa+hibernate缓存配置使用的文章。


JPA和hibernate的缓存关系,以及系统demo环境说明

JPA全称是:Java Persistence API



引用



JPA itself is just a specification, not a product; it cannot perform persistence or anything else by itself.

JPA仅仅只是一个规范,而不是产品;使用JPA本身是不能做到持久化的。




所以,JPA只是一系列定义好的持久化操作的接口,在系统中使用时,需要真正的实现者,在这里,我们使用Hibernate作为实现者。所以,还是用spring-data-jpa+hibernate4+spring3.2来做demo例子说明本文。




JPA规范中定义了很多的缓存类型:一级缓存,二级缓存,对象缓存,数据缓存,等等一系列概念,搞的人糊里糊涂,具体见这里:


http://en.wikibooks.org/wiki/Java_Persistence/Caching



不过缓存也必须要有实现,因为使用的是hibernate,所以基本只讨论hibernate提供的缓存实现。



很多其他的JPA实现者,比如toplink(EclipseLink),也许还有其他的各种缓存实现,在此就不说了。





先直接给出所有的demo例子



hibernate实现中只有三种缓存类型:



一级缓存,二级缓存和查询缓存。



在hibernate的实现概念里,他把什么集合缓存之类的统一放到二级缓存里去了。




1. 一级缓存测试:



文件配置:



jpa怎么加redis缓存 jpa有缓存吗_缓存



    1. <bean id="entityManagerFactory"
    2. class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">  
    3. "dataSource" ref="dataSource"
    4. "jpaVendorAdapter" ref="hibernateJpaVendorAdapter"
    5. "packagesToScan" value="com.restjplat.quickweb"
    6. "jpaProperties">  
    7.         <props>  
    8. "hibernate.show_sql">${hibernate.show_sql}</prop>  
    9. "hibernate.format_sql">true</prop>  
    10.         </props>  
    11.     </property>  
    12. </bean>




    可见没有添加任何配置项。



    jpa怎么加redis缓存 jpa有缓存吗_缓存



      1. private void
      2.     EntityManager em = emf.createEntityManager();  
      3. class, 1); //find id为1的对象
      4. class, 1); //find id为1的对象
      5. ""); //true
      6.   
      7.     EntityManager em1 = emf.createEntityManager();  
      8. class, 1); //find id为1的对象
      9.     EntityManager em2 = emf.createEntityManager();  
      10. class, 1); //find id为1的对象
      11. ""); //false
      12. }




      jpa怎么加redis缓存 jpa有缓存吗_缓存



        1. 输出为:因为sql语句打出来太长,所以用*号代替  
        2. Hibernate: ***********  
        3. 2014-03-17 20:41:44,819  INFO [main] (DictTest.java:76) - true
        4. Hibernate: ***********  
        5. Hibernate: ***********  
        6. 2014-03-17 20:41:44,869  INFO [main] (DictTest.java:84) - false




        由此可见:同一个session内部,一级缓存生效,同一个id的对象只有一个。不同session,一级缓存无效。



        2. 二级缓存测试:



        文件配置:



        1:实体类直接打上 javax.persistence.Cacheable 标记。


        jpa怎么加redis缓存 jpa有缓存吗_缓存



          1. @Entity
          2. @Table(name ="dict")  
          3. @Cacheable
          4. public class Dict extends




          2:配置文件修改,在 jpaProperties 下添加,用ehcache来实现二级缓存,另外因为加入了二级缓存,我们将hibernate的统计打开来看看到底是不是被缓存了。


          jpa怎么加redis缓存 jpa有缓存吗_缓存



            1. <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>  
            2. <prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop>  
            3. <prop key="hibernate.generate_statistics">true</prop>




            注1:如果在配置文件中加入了


            <prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop& gt;,则不需要在实体内配置hibernate的 @cache标记,只要打上JPA的@cacheable标记即可默认开启该实体的2级缓存。



            注2:如果不使用javax.persistence.sharedCache.mode配置,直接在实体内打@cache标记也可以。



            jpa怎么加redis缓存 jpa有缓存吗_缓存


            1. @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)  
            2. public class Dict extends



            至于 hibernate的 hibernate.cache.use_second_level_cache这个属性,文档里是这么写的:


            引用



            Can be used to completely disable the second level cache, which is enabled by default for classes which specify a <cache> mapping.

            即打上只要有@cache标记,自动开启。



            所以有两种方法配置开启二级缓存:



            第一种不使用hibernate的@cache标记,直接用@cacheable标记和缓存映射配置项。

            第二种用hibernate的@cache标记使用。




            另外javax.persistence.sharedCache.mode的其他配置如下:



            The javax.persistence.sharedCache.mode property can be set to one of the following values:


            • ENABLE_SELECTIVE (Default and recommended value): entities are not cached unless explicitly marked as cacheable.
            • DISABLE_SELECTIVE: entities are cached unless explicitly marked as not cacheable.
            • NONE: no entity are cached even if marked as cacheable. This option can make sense to disable second-level cache altogether.
            • ALL: all entities are always cached even if marked as non cacheable.

            如果用all的话,连实体上的@cacheable都不用打,直接默认全部开启二级缓存


            测试代码:



            jpa怎么加redis缓存 jpa有缓存吗_缓存


            1. private void
            2.         EntityManager em1 = emf.createEntityManager();  
            3. class, 1); //find id为1的对象
            4.         logger.info(d1.getName());  
            5.         em1.close();  
            6.           
            7.         EntityManager em2 = emf.createEntityManager();  
            8. class, 1); //find id为1的对象
            9.         logger.info(d2.getName());  
            10.         em2.close();  
            11.     }


            输出:


            jpa怎么加redis缓存 jpa有缓存吗_缓存


            1. Hibernate: **************  
            2. a  
            3. a  
            4. ===================L2======================  
            5. com.restjplat.quickweb.model.Dict : 1


            可见二级缓存生效了,只输出了一条sql语句,同时监控中也出现了数据。



            另外也可以看看如果是配置成ALL,并且把@cacheable删掉,输出如下:



            jpa怎么加redis缓存 jpa有缓存吗_缓存


            1. Hibernate: ************  
            2. a  
            3. a  
            4. ===================L2======================  
            5. com.restjplat.quickweb.model.Children : 0
            6. com.restjplat.quickweb.model.Dict : 1
            7. org.hibernate.cache.spi.UpdateTimestampsCache : 0
            8. org.hibernate.cache.internal.StandardQueryCache : 0
            9. com.restjplat.quickweb.model.Parent : 0
            10. =================query cache=================


            并且可以看见,所有的实体类都加入二级缓存中去了




            3. 查询缓存测试:



            一,二级缓存都是根据对象id来查找,如果需要加载一个List的时候,就需要用到查询缓存。



            在Spring-data-jpa实现中,也可以使用查询缓存。



            文件配置:


            在 jpaProperties 下添加,这里必须明确标出增加查询缓存。


            jpa怎么加redis缓存 jpa有缓存吗_缓存



            1. <prop key="hibernate.cache.use_query_cache">true</prop>  




            然后需要在方法内打上@QueryHint来实现查询缓存,我们写几个方法来测试如下:



            jpa怎么加redis缓存 jpa有缓存吗_缓存


            1. public interface DictDao extends
            2.   
            3. // spring-data-jpa默认继承实现的一些方法,实现类为
            4. // SimpleJpaRepository。
            5. // 该类中的方法不能通过@QueryHint来实现查询缓存。
            6. @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })  
            7.     List<Dict> findAll();  
            8.       
            9. @Query("from Dict")  
            10. @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })  
            11.     List<Dict> findAllCached();  
            12.       
            13. @Query("select t from Dict t where t.name = ?1")  
            14. @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })  
            15.     Dict findDictByName(String name);  
            16. }



            测试方法


            jpa怎么加redis缓存 jpa有缓存吗_缓存



              1. private void
              2. //无效的spring-data-jpa实现的接口方法
              3. //输出两条sql语句
              4.         dao.findAll();  
              5.         dao.findAll();  
              6. "================test 1 finish======================");  
              7. //自己实现的dao方法可以被查询缓存
              8. //输出一条sql语句
              9.         dao.findAllCached();  
              10.         dao.findAllCached();  
              11. "================test 2 finish======================");  
              12. //自己实现的dao方法可以被查询缓存
              13. //输出一条sql语句
              14. "a");  
              15. "a");  
              16. "================test 3 finish======================");  
              17.     }




              输出结果:

              1. Hibernate: **************  
              2. Hibernate: **************  
              3. ================test 1
              4. Hibernate: ***********  
              5. ================test 2
              6. Hibernate: ***********  
              7. ================test 3
              8. ===================L2======================  
              9. com.restjplat.quickweb.model.Dict : 5
              10. org.hibernate.cache.spi.UpdateTimestampsCache : 0
              11. org.hibernate.cache.internal.StandardQueryCache : 2
              12. =================query cache=================  
              13. select t from Dict t where t.name = ?1
              14. select generatedAlias0 from Dict as generatedAlias0  
              15. from Dict


              很明显,查询缓存生效。但是为什么第一种方法查询缓存无法生效,原因不明,只能后面看看源代码了。



              4.集合缓存测试:



              根据hibernate文档的写法,这个应该是算在2级缓存里面。



              测试类:


              jpa怎么加redis缓存 jpa有缓存吗_缓存



                1. @Entity
                2. @Table(name ="parent")  
                3. @Cacheable
                4. public class Parent extends
                5.       
                6. private static final long
                7. private
                8. private
                9.       
                10. public
                11. return
                12.     }  
                13. public void
                14. this.name = name;  
                15.     }  
                16.       
                17. @OneToMany(fetch = FetchType.EAGER,mappedBy = "parent")  
                18. @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)  
                19. public
                20. return
                21.     }  
                22. public void
                23. this.clist = clist;  
                24.     }  
                25. }  
                26.   
                27. @Entity
                28. @Table(name ="children")  
                29. @Cacheable
                30. public class Children extends
                31.   
                32. private static final long
                33. private
                34. private
                35.       
                36. @ManyToOne(fetch = FetchType.LAZY)  
                37. @JoinColumn(name = "parent_id")  
                38. public
                39. return
                40.     }  
                41.   
                42. public void
                43. this.parent = parent;  
                44.     }  
                45.   
                46. public
                47. return
                48.     }  
                49.   
                50. public void
                51. this.name = name;  
                52.     }     
                53. }



                测试方法:


                jpa怎么加redis缓存 jpa有缓存吗_缓存



                  1. private void
                  2.         EntityManager em1 = emf.createEntityManager();  
                  3. class, 1);  
                  4.         List<Children> c1 = p1.getClist();  
                  5.         em1.close();  
                  6. " ");  
                  7. for
                  8. ",");  
                  9.         }  
                  10.         System.out.println();  
                  11.         EntityManager em2 = emf.createEntityManager();  
                  12. class, 1);  
                  13.         List<Children> c2 = p2.getClist();  
                  14.         em2.close();  
                  15. " ");  
                  16. for
                  17. ",");  
                  18.         }  
                  19.         System.out.println();  
                  20.     }



                  输出:


                  jpa怎么加redis缓存 jpa有缓存吗_缓存


                  1. Hibernate: ********************  
                  2. Michael   
                  3. kate,Jam,Jason,Brain,  
                  4. Michael   
                  5. kate,Jam,Jason,Brain,  
                  6. ===================L2======================  
                  7. com.restjplat.quickweb.model.Children : 4
                  8. com.restjplat.quickweb.model.Dict : 0
                  9. org.hibernate.cache.spi.UpdateTimestampsCache : 0
                  10. com.restjplat.quickweb.model.Parent.clist : 1
                  11. org.hibernate.cache.internal.StandardQueryCache : 0
                  12. com.restjplat.quickweb.model.Parent : 1
                  13. =================query cache=================


                  在统计数据里可见二级缓存的对象数量。



                  本文我们不讨论关于缓存的更新策略,脏数据等等的东西,只是讲解配置方式。




                  接下来是源代码篇



                  理清楚各种配置以后,我们来看一下hibernate和spring-data-jpa的一些缓存实现源代码。



                  上面有个遗留问题,为什么spring-data-jpa默认实现的findAll()方法无法保存到查询缓存?只能啃源代码了。



                  打断点跟踪吧



                  入口方法是spring-data-jpa里的 SimpleJpaRepository类



                  jpa怎么加redis缓存 jpa有缓存吗_缓存

                  1. public
                  2. return getQuery(null, (Sort) null).getResultList();  
                  3.     }  
                  4.   
                  5. 然后到 QueryImpl<X>类的  
                  6. private
                  7. if (getEntityGraphQueryHint() != null) {  
                  8.             SessionImplementor sessionImpl = (SessionImplementor) getEntityManager().getSession();  
                  9. new HQLQueryPlan( getHibernateQuery().getQueryString(), false,  
                  10.                     sessionImpl.getEnabledFilters(), sessionImpl.getFactory(), getEntityGraphQueryHint() );  
                  11. // Safe to assume QueryImpl at this point.
                  12. class
                  13.         }  
                  14. return
                  15.     }  
                  16.   
                  17. 进入query.list();  
                  18.   
                  19. query类的代码解析google一下很多,于是直接到最后:  
                  20.   
                  21. 进入QueryLoader的list方法。  
                  22.   
                  23. protected
                  24. final
                  25. final
                  26. final
                  27. final Type[] resultTypes) throws
                  28.   
                  29. final boolean
                  30.             queryParameters.isCacheable();  
                  31.   
                  32. if
                  33. return
                  34.         }  
                  35. else
                  36. return
                  37.         }  
                  38.     }


                  果然有个cacheable,值为false,说明的确是没有从缓存里取数据。



                  用自定义的jpa查询方法测试后发现,这个值为true。



                  于是接着看cacheable的取值过程:



                  jpa怎么加redis缓存 jpa有缓存吗_缓存


                  1. final boolean
                  2.             queryParameters.isCacheable();



                  factory.getSettings().isQueryCacheEnabled() 这个一定是true,因为是在配置文件中打开的。那只能是queryParameters.isCacheable() 这个的问题了。



                  jpa怎么加redis缓存 jpa有缓存吗_缓存


                  1. 在query.list()的方法内部:  
                  2.   
                  3. public List list() throws
                  4.         verifyParameters();  
                  5.         Map namedParams = getNamedParams();  
                  6.         before();  
                  7. try
                  8. return
                  9.                     expandParameterLists(namedParams),  
                  10.                     getQueryParameters(namedParams)  
                  11.                 );  
                  12.         }  
                  13. finally
                  14.             after();  
                  15.         }  
                  16.     }  
                  17.   
                  18. getQueryParameters(namedParams)这个方法实际获取的是query对象的cacheable属性的值,也就是说,query对象新建的时候cacheable的值决定了这个query方法能不能被查询缓存。



                  接下来query的建立过程:



                  jpa怎么加redis缓存 jpa有缓存吗_缓存


                  1. 在 SimpleJpaRepository 类中 return
                  2.   
                  3. 直接由emcreate,再跟踪到 AbstractEntityManagerImpl中  
                  4.   
                  5. @Override
                  6. public
                  7.             String jpaqlString,  
                  8.             Class<T> resultClass,  
                  9.             Selection selection,  
                  10.             QueryOptions queryOptions) {  
                  11. try
                  12.             org.hibernate.Query hqlQuery = internalGetSession().createQuery( jpaqlString );  
                  13.   
                  14.             ....  
                  15. return new QueryImpl<T>( hqlQuery, this, queryOptions.getNamedParameterExplicitTypes() );  
                  16.         }  
                  17. catch
                  18. throw
                  19.         }  
                  20.     }  
                  21. 即通过session.createQuery(jpaqlString ) 创建初始化对象。  
                  22.   
                  23. 在query类定义中  
                  24. public abstract class AbstractQueryImpl implements
                  25.   
                  26. private boolean
                  27. }  
                  28. cacheable不是对象类型,而是基本类型,所以不赋值的情况下默认为“false”。



                  也就是说spring-data-jpa接口提供的简单快速的各种接口实现全是不能使用查询缓存的,完全不知道为什么这么设计。



                  接下来看看我们自己实现的查询方法实现:



                  直接找到query方法的setCacheable()方法打断点,因为肯定改变这个值才能有查询缓存。




                  jpa怎么加redis缓存 jpa有缓存吗_缓存

                  1. 于是跟踪到 SimpleJpaQuery类中  
                  2. protected
                  3. return
                  4. }



                  在返回query的过程中通过applyHints()方法读取了方法上的QueryHint注解从而设置了查询缓存。