引言:缓存一直以来都算是一个高大上的话题,因为其“用之得当,则事半功倍;反之,则事半功倍”。其实在使用ORM框架时,只谈执行SQL的执行效率来说,框架肯定是不如JDBC的。但用框架能减少数据层的维护,减少代码工作量,新手也可以更快的上手。而在性能方面,要想提高就需要借助三方的工具,如redis等,当然在譬如Hibernate框架中,其本身就有Cache的应用,不过如上所述,该缓存运用的好,可以提高应用的性能,同时还可以优化服务器的性能。本文主要对Hibernate的缓存机制进行进一步的认识。
在这2015年的最后一日,博客将用本文来做个结尾了。本文将从“一级缓存”开始,逐渐地迈向“二级缓存”,相关的项目配置也会随之改变,请留意阅读,注意前后的不同,相信定能对您有所帮助。
1. 准备
1.1 缓存原理
如下图,简单理解一下缓存:
1.2 说明及数据库搭建
首先需要说明的是,本文实例中使用的是Hibernate4版本,测试需要Junit4支持,缓存插件使用ehcache,数据库使用Mysql。
(1)相关JAR包
(2)在Mysql中创建一个名为hibernate的测试库,并且创建一张USER表,具体字段和初始化数据如下图
(3)本次示例主要用于测试缓存,自然没有增删之类的功能
(4)使用log4j日志输出,需要log4j的支持
(5)测试环境在Windows环境中,如果使用Mac或其他系统的,在ehcache.xml的配置文件中,请修改diskStore的路径
1.3 测试项目基础搭建(基于Hibernate4版本)
(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,ERRORFlog4j.appender.STDOUT=org.apache.log4j.ConsoleAppenderlog4j.appender.STDOUT.layout=org.apache.log4j.PatternLayoutlog4j.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*/@BeforeClasspublic 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测试类中添加如下方法:
/*** 测试一级缓存*/@Testpublic 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结束<<<<<<<<<<“);}
运行该方法,结果如下:
说明:从以上的结果看来,在第一个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”)@Testpublic 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结束<<<<<<<<<<“);}
运行该方法,结果如下:
说明:从以上结果来看,首先在第一次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” /><!– 设置缓存的默认数据过期策略 –><defaultCachemaxElementsInMemory=”10000″ eternal=”false” timeToIdleSeconds=”120″timeToLiveSeconds=”120″ overflowToDisk=”true” /></ehcache>
(3)在User实体添加缓存注解
如下:
/** 省略package和import,上面已列出 **//*** User实体类*/@Entity@Table(name=”user”)@Cacheablepublic class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private int id;private String name;private int age;/**省略get和set**/}
(4)在MainTest测试类中添加测试方法
如下:
/*** 测试二级缓存,单个对象*/@Testpublic 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)运行方法
结果如下:
说明:这里与一级缓存类似,就不做太多的输出了。结果也很明显,第二个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”)@Testpublic 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”)@Testpublic 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方法结果如下:
说明:在list中,显然,没有使用二级缓存,两个session中,都执行了SQL;而在Iterate中,与一级缓存类似,这里就不细说了。
(3)启用查询缓存
说明:在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)与其他应用共享的数据
如有问题,欢迎指出;如需转载,请标明出处,谢谢!
参考资料