螺 丝 钉 阅读(123) 评论(0)

    Hibernate中提供了对Cache的支持,用于减少一些必要的数据访问。这个功能如果能够正确的使用,程序性能会有很大的提升。但是很多时候,我们使用的可能不正确的。

Hibernate中Cache的类型

    Hibernate中提供了三种类型的Cache,这里的说法很可能与网上说法有些区别,网上的大多数说法是将Hibernate中的Cache分为一级缓存和二级缓存。我将他们分为3种:

1)Session Cache : 把对象缓存在current session对象中

2)Second Level Cache:用于跨Session存储对象。

3)Query Cache: 存储的是查询语句、参数、查询结果的ID集合。

 

接下来就分别说一说这三种缓存。

 

找了在Hibernate的Document中第一个例子:

public class Event {

   private Long id;
    private String title;
    private Date date;

   public Long getId() {
      return id;
   }

   public void setId(Long id) {
      this.id = id;
   }

   public String getTitle() {
      return title;
   }

   public void setTitle(String title) {

      this.title = title;

   }

   public Date getDate() {

      return date;

   }

   public void setDate(Date date) {

      this.date = date;

   }

   

   

}
View Code

 


 数据库描述:

 

1、Session Cache

 

Session Cache,就是将【实体】或者【实体集合】存储在Session中。

用例子说明Session Cache:

package com.hibernateTest.dao;

 
import java.util.Date;
import org.hibernate.Session;
import com.hibernateTest.entity.Event;
import util.HibernateUtil;
 
public class EventDao {

   public static void main(String[] args) {
      EventDao mgr = new EventDao();
      /*  if (args[0].equals("store")) {
            mgr.createAndStoreEvent("My Event", new Date());
        }
       */

        mgr.doubleGet();
        HibernateUtil.getSessionFactory().close();

    }

 

 

    private void createAndStoreEvent(String title, Date theDate) {

        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();
        Event theEvent = new Event();
        theEvent.setTitle(title);
        theEvent.setDate(theDate);
        session.save(theEvent);
        session.getTransaction().commit();
        session.close();
    }

    private void doubleGet() {

        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();
// 第一次查询

        Event theEvent =(Event)session.get(Event.class, 3L);
        System.out.println(theEvent.getTitle());
// 再次查询

        theEvent=(Event)session.get(Event.class, 3L);
        System.out.println(theEvent.getTitle());
        session.getTransaction().commit();
        session.close();
    }

}

 

 

先用SQL查询一下结果: 

 

 

也就是说title的值是My Event。接下来看程序执行结果(用EditPlus整理后):

 

 

也就是说执行了一次SQL查询。看来应该是第一次查询的结果被缓存了。既然说是Session Cache,那么应该就是缓存到Session对象中的某个变量里了。

 看看Session的结构:

 

根据上面的SessionImpl中的变量名来看,应该就是把数据缓存在persistenceContext中。不过这个靠猜是没有用的,还得通过调试来确认。

 

在调试过程中,发现了这么一个方法:

protected Object doLoad(

        final LoadEvent event,
        final EntityPersister persister,
        final EntityKey keyToLoad,
        final LoadEventListener.LoadType options) {

      if ( log.isTraceEnabled() ) {
        log.trace(
              "attempting to resolve: " +
              MessageHelper.infoString( persister, event.getEntityId(), event.getSession().getFactory() )
           );
      }

// 从Session Cache中获取实体
      Object entity = loadFromSessionCache( event, keyToLoad, options );
     if ( entity == REMOVED_ENTITY_MARKER ) {
        log.debug( "load request found matching entity in context, but it is scheduled for removal; returning null" );
        return null;
      }

      if ( entity == INCONSISTENT_RTN_CLASS_MARKER ) {
        log.debug( "load request found matching entity in context, but the matched entity was of an inconsistent return type; returning null" );
        return null;
      }

      if ( entity != null ) {
        if ( log.isTraceEnabled() ) {
           log.trace(
                 "resolved object in session cache: " +
                 MessageHelper.infoString( persister, event.getEntityId(), event.getSession().getFactory()  )

              );
        }
        return entity;
      }

// 从二级缓存中获取实体
      entity = loadFromSecondLevelCache(event, persister, options);
      if ( entity != null ) {
        if ( log.isTraceEnabled() ) {
           log.trace(
                 "resolved object in second-level cache: " +
                 MessageHelper.infoString( persister, event.getEntityId(), event.getSession().getFactory() )
              );
        }
        return entity;
      }

       if ( log.isTraceEnabled() ) {
        log.trace(
              "object not resolved in any cache: " +
              MessageHelper.infoString( persister, event.getEntityId(), event.getSession().getFactory() )
           );
      }

// 从数据源中获取实体

      return loadFromDatasource(event, persister, keyToLoad, options);

   }

 

上面的这个方法,很清晰的说明了Hibernate的查询过程。 

1)先从Session缓存查数据,2)查不到再从二级缓存中查数据,3)依旧差不多从数据源(例如XML文件、数据库等)加载数据。

 

从过程1)知道,确实会别的操作将数据存储到Session Cache中,不然怎么从Session Cache查数据呢。

再看看从Session Cache中是怎么查数据呢?

 

protected Object loadFromSessionCache(
        final LoadEvent event,
        final EntityKey keyToLoad,
        final LoadEventListener.LoadType options) throws HibernateException {

      SessionImplementor session = event.getSession();
      Object old = session.getEntityUsingInterceptor( keyToLoad );

      if ( old != null ) {
        // this object was already loaded
// 从Session对象中的persistenceContext属性中查询
        EntityEntry oldEntry = session.getPersistenceContext().getEntry( old );
        if ( options.isCheckDeleted() ) {
           Status status = oldEntry.getStatus();
           if ( status == Status.DELETED || status == Status.GONE ) {
              return REMOVED_ENTITY_MARKER;
           }
        }
        if ( options.isAllowNulls() ) {
//         EntityPersister persister = event.getSession().getFactory().getEntityPersister( event.getEntityClassName() );
           EntityPersister persister = event.getSession().getFactory().getEntityPersister( keyToLoad.getEntityName() );
           if ( ! persister.isInstance( old, event.getSession().getEntityMode() ) ) {
              return INCONSISTENT_RTN_CLASS_MARKER;
           }
        }

        upgradeLock( old, oldEntry, event.getLockOptions(), event.getSession() );
      }
 
      return old;

   }

既然是从session对象中的persistenceContext获取数据,那就说明前面讲到的Session Cache确实是persistenceContext,也验证了前面猜测的正确性。 

 

上面的例子中,做了两次查询,在第一次查询时,Session Cache中并没有相关的实体,因此会从数据库中获取数据。也就是说走的是loadFromDatasource方法。

     那么程序又是在何时将查询到的数据存储在Session Cache中,减少数据库访问次数呢?要知道这个问题的答案就要查看loadFromDatasource的实现了。

在对方法跟踪了N多层调用后找到了:

private void loadFromResultSet(

           final ResultSet rs,

           final int i,

           final Object object,

           final String instanceEntityName,

           final EntityKey key,

           final String rowIdAlias,

           final LockMode lockMode,

           final Loadable rootPersister,

           final SessionImplementor session)

   throws SQLException, HibernateException {

 

      final Serializable id = key.getIdentifier();

 

      // Get the persister for the _subclass_

      final Loadable persister = (Loadable) getFactory().getEntityPersister( instanceEntityName );

 

      if ( log.isTraceEnabled() ) {

        log.trace(

              "Initializing object from ResultSet: " +

              MessageHelper.infoString( persister, id, getFactory() )

           );

      }

     

      boolean eagerPropertyFetch = isEagerPropertyFetchEnabled(i);

 

      // add temp entry so that the next step is circular-reference

      // safe - only needed because some types don't take proper

      // advantage of two-phase-load (esp. components)

// 两阶段加载,将数据存储到session cache中

      TwoPhaseLoad.addUninitializedEntity(

           key,

           object,

           persister,

           lockMode,

           !eagerPropertyFetch,

           session

        );

 

      //This is not very nice (and quite slow):

      final String[][] cols = persister == rootPersister ?

           getEntityAliases()[i].getSuffixedPropertyAliases() :

           getEntityAliases()[i].getSuffixedPropertyAliases(persister);

 

      final Object[] values = persister.hydrate(

           rs,

           id,

           object,

           rootPersister,

           cols,

           eagerPropertyFetch,

           session

        );

 

      final Object rowId = persister.hasRowId() ? rs.getObject(rowIdAlias) : null;

 

      final AssociationType[] ownerAssociationTypes = getOwnerAssociationTypes();

      if ( ownerAssociationTypes != null && ownerAssociationTypes[i] != null ) {

        String ukName = ownerAssociationTypes[i].getRHSUniqueKeyPropertyName();

        if (ukName!=null) {

           final int index = ( (UniqueKeyLoadable) persister ).getPropertyIndex(ukName);

           final Type type = persister.getPropertyTypes()[index];

  

           // polymorphism not really handled completely correctly,

           // perhaps...well, actually its ok, assuming that the

           // entity name used in the lookup is the same as the

           // the one used here, which it will be

  

           EntityUniqueKey euk = new EntityUniqueKey(

                 rootPersister.getEntityName(), //polymorphism comment above

                 ukName,

                 type.semiResolve( values[index], session, object ),

                 type,

                 session.getEntityMode(), session.getFactory()

              );

           session.getPersistenceContext().addEntity( euk, object );

        }

      }

 

      TwoPhaseLoad.postHydrate(

           persister,

           id,

           values,

           rowId,

           object,

           lockMode,

           !eagerPropertyFetch,

           session

        );

 

   }
View Code

  

public static void addUninitializedEntity(

        final EntityKey key,
        final Object object,
        final EntityPersister persister,
        final LockMode lockMode,
        final boolean lazyPropertiesAreUnfetched,
        final SessionImplementor session

   ) {

      session.getPersistenceContext().addEntity(
           object,
           Status.LOADING,
           null,
           key,
           null,
           lockMode,
           true,
           persister,
           false,
           lazyPropertiesAreUnfetched

        );

   }
View Code

get,load,save,saveOrUpdate,update,merge。。。等等都是会被存储到Session Cache中的。

 

 

2、Second Level Cache

    Session Cache,我们认为是Hibernate的默认缓存,它是一个事务级别的缓存。但是这肯定不能满足实际的项目需求。Hibernate还设计了SessionFactory级别的缓存,也叫做二级缓存(相对于Session Cache来说)。

 要使用二级缓存,得有缓存提供商、缓存策略等配置。

Cache Providers

常见的缓存提供商有:

 

其中HashableCacheProvider是Hibernate默认提供的二级缓存。学习二级缓存时可以用用,但是不建议在生产环境下使用。

在SessionFactory中,会存储系统要使用的各个类。我们的项目中,也不是所有的类都要用到缓存的。所有Hibernate的设计就是让我们可以配置那些类使用缓存。

然后在Mapping文件中添加<cache>配置,配置的语法:

 

 

useage,用于指定使用哪种策略的缓存。

region,是用于指定二级缓存的区域范围。

include,用于和Fetch策略配合。

各个缓存提供商对于这几种缓存策略的支持:

 

在配置了这些之后,程序还是和之前的写法一样的。

 

 

3、Query Cache

多用于存储数据表数据,如果要存储实体,也是可以的。

如果要使用Query Cache,接下来进行下面的操作:

<property name="hibernate.cache.use_query_cache">true</property>

并在查询之前设置cacheable为true即可,(也就是Query或者Criterion的setCacheable(true)即可)。

 

综合起来,就是需要做下面的工作:

1)在hibernate.cfg.xml中配置Cache Provider

2)在hibernate.cfg.xml中配置查询缓存:

<property name="hibernate.cache.use_query_cache">true</property>

3)在程序中使用setCacheable(true)。

 

查询缓存存储的是:查询HQL、参数、查询结果的实体的ID

 

Query的查询的过程说明:

在根据Hibernate的查询实现时找到:

protected List list(

           final SessionImplementor session,

           final QueryParameters queryParameters,

           final Set querySpaces,

           final Type[] resultTypes) throws HibernateException {

 

      final boolean cacheable = factory.getSettings().isQueryCacheEnabled() &&

        queryParameters.isCacheable();

// 如果使用缓存则从缓存中查询

      if ( cacheable ) {

        return listUsingQueryCache( session, queryParameters, querySpaces, resultTypes );

      }

      else {

        return listIgnoreQueryCache( session, queryParameters );

      }

   }

 

 

// 从缓存查询

private List listUsingQueryCache(

        final SessionImplementor session,

        final QueryParameters queryParameters,

        final Set querySpaces,

        final Type[] resultTypes) {

   // 从SessionFactory中拿到QueryCache

      QueryCache queryCache = factory.getQueryCache( queryParameters.getCacheRegion() );

     

      Set filterKeys = FilterKey.createFilterKeys(

           session.getLoadQueryInfluencers().getEnabledFilters(),

           session.getEntityMode()

      );

      QueryKey key = QueryKey.generateQueryKey(

           getSQLString(),

           queryParameters,

           filterKeys,

           session,

           ( areResultSetRowsTransformedImmediately( queryParameters.getResultTransformer() ) ?

                 queryParameters.getResultTransformer() :

                 null

           )

      );

     

      if ( querySpaces == null || querySpaces.size() == 0 ) {

        log.trace( "unexpected querySpaces is "+( querySpaces == null ? "null" : "empty" ) );

      }

      else {

        log.trace( "querySpaces is "+querySpaces.toString() );

      }

// 从查询缓存中查询

      List result = getResultFromQueryCache(

           session,

           queryParameters,

           querySpaces,

           resultTypes,

           queryCache,

           key

        );

// 没有查到则从数据库直接查询

      if ( result == null ) {

        result = doList( session, queryParameters );

// 取数完毕缓存查询信息

        putResultInQueryCache(

              session,

              queryParameters,

              resultTypes,

              queryCache,

              key,

              result

        );

      }

 

 

      return getResultList( result, queryParameters.getResultTransformer() );

   }
View Code

 

从上面的过程了解到使用Query查询时,会先在查询缓存中看看是否已经有同样的查询执行,如果有,则从查询缓存中取数。如果从缓存中没有差得数据,则从数据库中取数。 

 如果是从数据库取数,取数完毕会将查询的结果放到查询缓存中。

其实放到查询缓存中的不是查询结果的全部,而是查询到的实体的ID。这个可以通过debug看出。

 

 

Second Level Cache与Query Cache

接下来就用例子来说二级缓存与查询缓存:

例子1:不使用二级缓存和查询缓存

<property name="cache.use_second_level_cache">false</property>
<property name="cache.use_query_cache">false</property>
public void getData(){

      Session session=HibernateUtil.getSessionFactory().getCurrentSession();
      session.beginTransaction();
      Query query=session.createQuery("from Event e where id>0");

   // query.setCacheable(true);

      List<Event> list=query.list();
      System.out.println(list.size());
      System.out.println(list.get(0).getTitle());
      session.getTransaction().commit();
   }

   public static void main(String[] args) {
      EventDao mgr=new EventDao();
      mgr.getData();
      mgr.getData();

   }

 

这个例子,不使用查询缓存也不使用二级缓存。每次执行完毕getData(),Session就会被自动关闭。如此来查探Hibernate的SessionFactory级别的缓存(二级缓存和查询缓存)。 

执行了两次查询。

 

例子2:只是用二级缓存,不使用查询缓存

<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">false</property>

程序代码不变,执行的结果是: 

仍然是执行了两次SQL,二级缓存中应当是有数据的,但是依旧还执行了两次,我们可以猜测二级缓存只缓存了查询结果,没有向查询缓存那样存储查询语句,参数等信息。

 

验证一下猜测,修改代码为:

public void getData(){
      Session session=HibernateUtil.getSessionFactory().getCurrentSession();
      session.beginTransaction();
      Query query=session.createQuery("from Event e where id>0");
   // query.setCacheable(true);

      List<Event> list=query.list();
      System.out.println(list.size());
      System.out.println(list.get(0).getTitle());
      session.getTransaction().commit();

     

      System.out.println(session.isOpen());

     

      session=HibernateUtil.getSessionFactory().getCurrentSession();
      session.beginTransaction();
      Event event=(Event) session.get(Event.class, 1L);
      System.out.println(event.getTitle());
      session.getTransaction().commit();
  

      System.out.println();

   }

修改mgr.getData(),在mgr.getData()中,重新获取一个Session,直接使用session.get(),这样保证执行session.get()时,是用了一个新的session,避免了Session Cache的影响。程序使用了二级缓存,如果在执行一次mgr.getData()时,执行session.get时,没有执行SQL,说明数据确实被存储到二级缓存中了。如果两次mgr.getData()都在执行query.list查询时,说明二级缓存中只是存储了查询结果,没有存储查询的信息(HQL语句、参数、结果中实体的ID)。接下来看看执行结果: 

 

这样的结果证实了上面的猜测。

 

例子3:只是用查询缓存:

调整缓存配置,开启查询缓存,关闭二级缓存:

<property name="cache.use_second_level_cache">false</property>
<property name="cache.use_query_cache">true</property>

 并去掉getData方法中setCacheable之前的注释。

执行结果:

 

第一次执行mgr.getData()的过程,

 

1)执行query.list()进行了一次查询。查询出结果,保存查询信息。

2)执行session.get(1)时,因为没有使用二级缓存,这又是一个新的session,所以会从数据库查询。

 

第二次执行mgr.getData的过程:

1)执行query.list()时,发现这是已经查询过的,因为查询缓存中存储了查询的相关信息,也就是说已经存储了那些实体的ID,但是没有存储相应的实体。需要做的只是根据实体的ID查询出实体即可。所以会看到三条根据id查询实体的sql语句。

2)执session.get(1)时,同上。

 

这个测试验证了查询缓存确实是存储了HQL、结果ID。因为这个查询中没有指定参数,并没有验证查询缓存也存储了查询参数,如果想要知道是否存储了查询参数,可以自己做个测试。

 

 

例子4:查询缓存与二级缓存都开启

<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">true</property>

程序代码不做修改,执行结果: 

 

第一次执行getData():

1)执行query.list时发起sql查询。

2)执行session.get时发现二级缓存中已有该实体,就直接取出,不需要查询

 

第二次执行getData():

1)执行query.list时,发现这个HQL已经执行过,所以就不会发起SQL查询去结果集。就取出已存储的结果的id,因为也配置了二级缓存,所以会拿着这个id去二级缓存中找找有没有,在二级缓存中也有这条记录,就直接取出来,不需要从数据库查询了。把从二级缓存中取得的数据放到list中。

2)session.get(1)时,去二级缓存中也查到了这条记录,所有就直接取出,也没有SQL执行。

 

从上面例子可以看出,使用二级缓存和Query 缓存结合,效率应为最高。 

缓存管理

 

1)Session 缓存管理

 

在使用Session管理实体时,如果使用了save()、update()、saveOrUpdate()、load()、get()、list()、iterate()、scroll()方法时,处理的实体都会被保存到Session Cache中。

当调用flush()时,这些对象将被同步到数据库中。如果不想把数据同步到数据库中,或者是要查询出大量的数据,都可以使用evict()将数据从Session Cache中移除。

如果要清除Session Cache中所有的数据,使用clear()。

 

2)二级缓存管理

其实在配置了二级缓存策略后,二级缓存的管理,就交给了Hibernate了,我们编程时不会涉及到二级缓存的管理。

如果真的想在程序中管理二级缓存,可以使用SessionFactory中的evict方法或者evictCollection方法。

3)Session缓存与二级缓存如何交互

 

Hibernate控制Session缓存与二级缓存交互的工具是CacheModel。

 

如果配置为NORMAL模式:

执行查询时,

从Session缓存查询,没有查到从二级缓存中查询,还没有查到,从数据库查。从数据库查询的数据会保存到Session缓存和二级缓存中。

更新数据时,也会把Session中的数据更新到二级缓存中和数据中。

 

GET模式,

执行查询时,同上。

更新数据时,不会更新到二级缓存中,只更新到数据库中。

 

 

4)查看二级缓存或者查询缓存中的实体

 

 

regionName是你在配置文件中配置的。