HibernateORM性能优化技术笔录资料整理随笔日志

Hibernate Cache的深入认识.

FavoriteLoadingAdd to favorites
引言:缓存一直以来都算是一个高大上的话题,因为其“用之得当,则事半功倍;反之,则事半功倍”。其实在使用ORM框架时,只谈执行SQL的执行效率来说,框架肯定是不如JDBC的。但用框架能减少数据层的维护,减少代码工作量,新手也可以更快的上手。而在性能方面,要想提高就需要借助三方的工具,如redis等,当然在譬如Hibernate框架中,其本身就有Cache的应用,不过如上所述,该缓存运用的好,可以提高应用的性能,同时还可以优化服务器的性能。本文主要对Hibernate的缓存机制进行进一步的认识。
 
在这2015年的最后一日,博客将用本文来做个结尾了。本文将从“一级缓存”开始,逐渐地迈向“二级缓存”,相关的项目配置也会随之改变,请留意阅读,注意前后的不同,相信定能对您有所帮助。
 
1. 准备
 
1.1 缓存原理
 
如下图,简单理解一下缓存:
缓存原理
 
1.2 说明及数据库搭建
 
首先需要说明的是,本文实例中使用的是Hibernate4版本,测试需要Junit4支持,缓存插件使用ehcache,数据库使用Mysql。
(1)相关JAR包
(2)在Mysql中创建一个名为hibernate的测试库,并且创建一张USER表,具体字段和初始化数据如下图
Hibernate缓存测试数据
(3)本次示例主要用于测试缓存,自然没有增删之类的功能
(4)使用log4j日志输出,需要log4j的支持
(5)测试环境在Windows环境中,如果使用Mac或其他系统的,在ehcache.xml的配置文件中,请修改diskStore的路径
 
1.3 测试项目基础搭建(基于Hibernate4版本) 
 
如果您想自行动手测试的话,请创建好相关数据库。下面先看一下我们需要准备的项目代码。
 
(1)示例项目结构(最终版)
Hibernate示例结构
(2)实体对象User(使用注解模式)
代码:
package com.hibernate.entity;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
@Entity
@Table(name=”user”)
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private int age;
 
    /** 省略get和set **/
 
}
(3)log4j.properties
配置:
log4j.rootLogger=INFO,STDOUT,ERRORF
log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=[%p]-%m%n
(4)hibernate.cfg.xml
配置:
<?xml version=’1.0′ encoding=’utf-8′?>
<!DOCTYPE hibernate-configuration PUBLIC 
        “-//Hibernate/Hibernate Configuration DTD 3.0//EN” 
<hibernate-configuration>
    <session-factory>
 
        <!– 基础配置 –>
        <property name=”connection.driver_class”>com.mysql.jdbc.Driver</property>
        <property name=”connection.url”>jdbc:mysql://localhost:3306/hibernate</property>
        <property name=”connection.username”>root</property>
        <property name=”connection.password”>tsky</property>
        <property name=”show_sql”>true</property>
        <property name=”hbm2ddl.auto”>update</property>
        <property name=”connection.pool_size”>20</property>
        <property name=”jdbc.fetch_size”>50</property>
        <property name=”jdbc.batch_size”>23</property> 
        <property name=”dialect”>org.hibernate.dialect.MySQL5Dialect</property>
        <!– 配置管理 Session 的方式 –>
        <property name=”current_session_context_class”>thread</property>
 
        <!– 实体 –>
        <mapping class=”com.hibernate.entity.User” />
 
    </session-factory>
</hibernate-configuration>
说明:因为是循序渐进的,此处没有配置二级缓存,只是正常的Hibernate配置。
(5)创建测试类MainTest
代码:
package com.hibernate.test;
 
import java.util.Iterator;
import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;
import org.hibernate.stat.Statistics;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hibernate.entity.User;
 
/**
 * 测试类
 */
public class MainTest {
 
    private static SessionFactory sessionFactory = null;
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    /**
     * 获取sessionFactory
     * @throws HibernateException
     */
    @BeforeClass
    public static void beforeClass() throws HibernateException {
        Configuration configuration = new Configuration();
        configuration.configure(“/hibernate.cfg.xml”);
        //hibernate4的session获取方法,与之前有所不同
        ServiceRegistry serviceRegistry = new ServiceRegistryBuilder().applySettings(configuration.getProperties()).buildServiceRegistry();  
        sessionFactory = configuration.buildSessionFactory(serviceRegistry);
    }
 
}
说明:以上只列出了@BeforeClass的方法,具体的@Test方法就直接放在相关知识点处,方便理解,同时也把后续需要的类都import进来了。
 
2. 一级缓存
 
2.1 简介
 
(1)称之为“Session缓存”和“会话级缓存”
(2)通过Session从数据库查询实体时,会将实体存储在内存中,下一次查询统一实体时,就不再从数据库中查询,直接从内存中获取(注:一定要是同一个Session)
(3)一级缓存的生命周期和Session相同,Session销毁,缓存也就销毁;同理一级缓存中数据只适用在当前会话之内
(4)默认开启的,也是强制开启的,不可关闭
 
2.2 常见API
 
(1)evict():将某个对象从Session中的一级缓存中清除
(2)clear():将一级缓存中的所有对象全部清除
(3)contains():判断指定的对象是否存在于一级缓存中
(4)flush():刷新一级缓存区的内容,使之与数据库数据保持同步
 
2.3 编写测试及说明(单个对象
 
下面就来验证一下上述的“一级缓存”吧。
在其他配置不改动的情况下,在MainTest测试类中添加如下方法:
/**
* 测试一级缓存
*/
@Test
public void testSessionCache() {
    logger.info(“********测试一级缓存********”);
    logger.info(“>>>>>>>>>>第一个Session”);
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    logger.info(“–第一次查询–“);
    User u1 = (User) session.load(User.class, 1);//find id为1的对象
    logger.info(u1.getName());
    logger.info(“–第二次查询–“);
    User u2 = (User) session.load(User.class, 1);
    logger.info(u2.getName());
    session.getTransaction().commit();
    session.close();
    logger.info(“第一个Session结束<<<<<<<<<<“);
    logger.info(“>>>>>>>>>>第二个Session”);
    Session session2 = sessionFactory.openSession();
    session2.beginTransaction();
    logger.info(“–第一次查询–“);
    User u3 = (User) session2.load(User.class, 1);
    logger.info(u3.getName());
    logger.info(“–清除对象缓存–“);
    //清除上面查询的对象缓存
    session2.evict(u3);
//    session2.clear();
    logger.info(“–第二次查询–“);
    User u4 = (User) session2.load(User.class, 1);
    logger.info(u4.getName());
    session2.getTransaction().commit();
    session2.close();
    logger.info(“第二个Session结束<<<<<<<<<<“);
}
运行该方法,结果如下:
 Hibernate一级缓存结果1
 
说明:从以上的结果看来,在第一个session中,第一次查询时,肯定是要执行SQL查询,第二次查询时,没有执行SQL,直接读取了一级缓存;再看第二个Session中,因为中间运行了evict或clear方法对一级缓存进行了清除,所以第二次查询时,仍需要实行SQL。
 
2.4 Query中list方法和iterate方法的区别
 
(1)list方法会将所有的结果集存放在内存中(可能会导致内存过高或者溢出),而iterate是将使用到的结果集放在内存中,但首次使用的时候,都会去数据库查询一次(执行次数可能N+1次)
(2)list方法每次不读取一级缓存,iterate会使用查询语句得到ID值的列表,如果缓存中存在则获取,若没有则进行数据库查询
 
注:现在都处于一级缓存,未开启二级缓存
 
2.5 编写测试及说明(Query列表
依然是在MainTest测试类中添加如下方法:
/**
* 测试一级缓存中的Query
*/
@SuppressWarnings(“unchecked”)
@Test
public void testQuery() {
    logger.info(“********测试一级缓存中,Query的list和iterate区别********”);
    logger.info(“>>>>>>>>>>第一个Session”);
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    Query query1 = session.createQuery(“from User”);
    logger.info(“>>>>>>>>>>使用Query的list方法”);
    logger.info(“–第一次查询–“);
    List<User> userList = query1.list();
    for (User user : userList) {
        logger.info(user.getName());
    }
    logger.info(“–第二次查询–“);
    List<User> userList2 = query1.list();
    for (User user : userList2) {
        logger.info(user.getName());
    }
    logger.info(“Query的list方法结束<<<<<<<<<<“);
    session.getTransaction().commit();
    session.close();
    logger.info(“第一个Session结束<<<<<<<<<<“);
    logger.info(“>>>>>>>>>>第二个Session”);
    Session session2 = sessionFactory.openSession();
    session2.beginTransaction();
    Query query2 = session2.createQuery(“from User”);
    logger.info(“>>>>>>>>>>使用Query的iterate方法”);
    logger.info(“–第一次查询–“);
    Iterator<User> it = query2.iterate();
    while (it.hasNext()) {
        User user = it.next();
        logger.info(user.getName());
    }
    logger.info(“–第二次查询–“);
    Iterator<User> it2 = query2.iterate();
    while (it2.hasNext()) {
        User user = it2.next();
        logger.info(user.getName());
    }
    logger.info(“Query的iterate方法结束<<<<<<<<<<“);
    session2.getTransaction().commit();
    session2.close();
    logger.info(“第二个Session结束<<<<<<<<<<“);
}
运行该方法,结果如下: 
 Hibernate一级缓存结果2
 
说明:从以上结果来看,首先在第一次session中,使用的是query.list(),直接看出list不会存放于一级缓存;第二次缓存中,使用query.iterate(),第一次查询时,首先执行了一级缓存中id序列表的查询,都没有查到,当运行User对象输出时,执行了两次SQL,表明,iterate()方法,每次使用对象时,才会去查找对象,不是预先放在内存中的,第二次查询时首先还是查询id序列表,下面两个对象都已存在于一级缓存中,所以不需要执行SQL。
 
3. 二级缓存
3.1 简介
(1)可称之为“应用级缓存”
(2)每个Session共用的缓存
(3)常用的二级缓存插件
         EHCache(本文使用)、HashTable、OSCache、SwarmCahe和JBossCache
3.2 缓存启用步骤
(1)添加相关的JAR包(ehcache-core-2.4.3.jar、hibernate-ehcache-4.2.4.Final.jar)
(2)在配置文件中添加Provider的描述
(3)添加二级缓存的属性配置文件
(4)在需要被缓存的实体上配置cache标签
3.3 编写测试及说明(单个对象)
(1)在hibernate.cfg.xml添加二级缓存配置
如下:
<!– 开启二级缓存 –>
<property name=”cache.use_second_level_cache”>true</property>
<property name=”cache.region.factory_class”>org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<!– 缓存全局策略,设置为ENABLE_SELECTIVE,在实体上注释@Cacheable即可;ALL的话,就是默认全部开启;DISABLE_SELECTIVE;NONE –>
<property name=”javax.persistence.sharedCache.mode”>ENABLE_SELECTIVE</property>
(2)添加ehcache.xml配置
如下:
<ehcache>
    <!– EHCache缓存溢出时,会把数据写到硬盘上, 且将把数据写到这个目录下 –>
    <diskStore path=”d:\\tempDirectory” />
    <!– 设置缓存的默认数据过期策略 –>
    <defaultCache 
        maxElementsInMemory=”10000″ eternal=”false” timeToIdleSeconds=”120″ 
        timeToLiveSeconds=”120″ overflowToDisk=”true” />
</ehcache>
(3)在User实体添加缓存注解
 如下:
/** 省略package和import,上面已列出 **/
/**
 * User实体类
 */
@Entity
@Table(name=”user”)
@Cacheable
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private int age;
    /**省略get和set**/
}
(4)在MainTest测试类中添加测试方法
如下:
/**
* 测试二级缓存,单个对象
*/
@Test
public void testEhcache() {
    logger.info(“********测试二级缓存********”);
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    User u1 = (User) session.load(User.class, 1);
    logger.info(u1.getName());
    session.getTransaction().commit();
    session.close();
    Session session2 = sessionFactory.openSession();
    session2.beginTransaction();
    User u2 = (User) session2.load(User.class, 1);
    logger.info(u2.getName());
    session2.getTransaction().commit();
    session2.close();
}
(5)运行方法
结果如下:
Hibernate二级缓存结果1
 
说明:这里与一级缓存类似,就不做太多的输出了。结果也很明显,第二个session中,没有执行SQL,直接读的二级缓存。
 
3.4 查询缓存(主要用在list上)
 
将hibernate.cache.use_query_cache设置为true,这样findAll()、 list()、Iterator()、createCriteria()、createQuery()等方法,就可以使用缓存中的数据集了,不设置的话,hibernate只会缓存使用load()方法获得的单个持久化对象。
配置如下:
<!– 开启查询缓存,支持list的缓存查询 –>
<property name=”cache.use_query_cache”>true</property>
(1)在MainTest测试类中添加测试方法
测试list:
/**
* 测试二级缓存list
*/
@SuppressWarnings(“unchecked”)
@Test  
public void testEhcacheList() {
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    List<User> users1 = (List<User>)session.createQuery(“from User”).setCacheable(true).list();
    for(User user : users1) {
        logger.info(user.getName());
    }
    session.getTransaction().commit();
    session.close();
    Session session2 = sessionFactory.openSession();
    session2.beginTransaction();
    List<User> users2 = (List<User>)session2.createQuery(“from User”).setCacheable(true).list();
    for(User user : users2) {
        logger.info(user.getName());
    }
    session2.getTransaction().commit();
    session2.close();
}
测试iterate
/**
* 测试二级缓存iterate
*/
@SuppressWarnings(“unchecked”)
@Test  
public void testEhcacheIterate() {
    Session session = sessionFactory.openSession();
    session.beginTransaction();
    Query query1 = session.createQuery(“from User”);
    Iterator<User> it1 = query1.iterate();
    while (it1.hasNext()) {
        User user = it1.next();
        logger.info(user.getName());
    }
    session.getTransaction().commit();
    session.close();
 
    Session session2 = sessionFactory.openSession();
    session2.beginTransaction();
    Query query2 = session2.createQuery(“from User”);
    Iterator<User> it2 = query2.iterate();
    while (it2.hasNext()) {
        User user = it2.next();
        logger.info(user.getName());
    }
    session2.getTransaction().commit();
    session2.close();
 
}
(2)不启用查询缓存
 
执行testEhcacheList方法结果如下:
Hibernate二级缓存结果2
执行ttestEhcacheIterate方法结果如下: Hibernate二级缓存结果3
 
说明:在list中,显然,没有使用二级缓存,两个session中,都执行了SQL;而在Iterate中,与一级缓存类似,这里就不细说了。
 
(3)启用查询缓存
 
执行testEhcacheList方法结果如下:
 Hibernate二级缓存结果4
执行ttestEhcacheIterate方法结果如下:
Hibernate二级缓存结果5 
说明:在list中,只在第一个session中执行了SQL,后面全部在二级缓存中获取;而在Iterate中,没有什么变化,因为其机制不同,是单个对象的缓存。
 
3.5 get和load的区别
 
get()方法和load()方法的区别在于对二级缓存的使用上。
load()方法会使用二级缓存,而get()方法在一级缓存没有找到的情况下会直接查询数据库,不会去二级缓存中查找。在使用中,对使用了二级缓存的对象进行查询时最好使用load()方法,以充分利用二级缓存来提高检索的效率。
 
3.6 实体中的Cache配置
 
有三个属性,如下
(1)usage:指定缓存策略,包含READ_ONLY,READ_WRITE,TRANSACTIONAL等
(2)include:指是否缓存加载延迟加载的对象,all表示缓存所有对象,non-lazy表示不缓存
(3)region:为实体配置指定的缓存策略。
 
在ehcache.xml中配置指定的策略。
配置添加如下:
<!– 针对User单个对象进行配置缓存策略 –>
<cache name=”UserCache”
    maxElementsInMemory=”1″ eternal=”false” timeToIdleSeconds=”300″
    timeToLiveSeconds=”600″ overflowToDisk=”true” />
 
同时在User中修改注解:
将@Cacheable改为@Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region=”UserCache”)即可,结果类似。
 
3.7 什么样的数据适合存放到第二级缓存中?
 
(1)很少被修改的数据
(2)不是很重要的数据,允许出现偶尔并发的数据
(3)不会被并发访问的数据
(4)常量数据
 
3.8 不适合存放到第二级缓存的数据?
 
(1)经常被修改的数据
(2)绝对不允许出现并发访问的数据,如财务数据
(3)与其他应用共享的数据
 
4. 一级缓存和二级缓存的比较
 
总结如下:
Hibernate缓存对比