hibernate集成ehcahe进行缓存管理
ehcace是现在非常流行的缓存框架,有轻量、灵活、可扩展、支持集群/分布式等优点。
在项目中,使用ehcace可以对数据进行缓存(一般使用、基于注解、基于aop),使用filter可以对页面进行缓存(SimplePageCachingFilter过滤器),与hibernate整合可以对对象进行缓存(二级缓存、查询缓存)。
简单的说使用缓存的方式主要分为数据层缓存、服务层缓存和页面缓存三种,它们一层比一层高效,实现也越来越复杂,在实际应用中最好能在尽量靠近用户的地方缓存,减少之后各层处理的压力,提高响应速度。
这篇文章先介绍hibernate的部分:二级缓存和查询缓存。
一、二级缓存
hibernate是自带一级缓存(session级别、事务级缓存)的,在一次请求中查询出的对象会被缓存,之后使用这个对象的时候会从缓存中取(不必多次访问数据库了)。
不过在这次请求处理结束、session关闭后,缓存中的数据就被清除了,第二次请求里用到的话还是需要再查一次。
如果想缓存一次还可以共享给之后的请求,就需要hibernate开启二级缓存了(sessionFactory级别、应用级缓存),它是跨session的,由sessionFactroy管理。
不过hibernate没有提供相应的二级缓存组件,需要加入额外的二级缓存包,常用的就是ehcache了,下面是hibernate集成ehcache进行二级缓存的配置方法(用一个较早的demo版本作为基础):
1、添加jar包,修改pom.xml文件,加入:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId> <version>4.2.21.Final</version> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache-core</artifactId> <version>2.6.11</version> </dependency>
2、修改spring-context-hibernate.xml,在hibernateProperties里增加3行配置:
<property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop> <prop key="hibernate.hbm2ddl.auto">none</prop> <prop key="hibernate.show_sql">${hibernate.show_sql}</prop> <prop key="hibernate.format_sql">false</prop> <!-- 开启二级缓存 --> <prop key="hibernate.cache.use_second_level_cache">true</prop> <!-- 二级缓存的提供类 --> <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop> <!-- 二级缓存配置文件的位置 --> <prop key="net.sf.ehcache.configurationResourceName">ehcache-hibernate.xml</prop> </props> </property>
3、在"src/main/resources"代码文件夹中新建文件"ehcache-hibernate.xml",内容为:
<?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false"> <!-- Cache配置项说明 必须项 name 非默认Cache配置的名称,唯一的 maxEntriesLocalHeap 在内存中缓存的最大对象数(默认值为0,表示不限制) maxEntriesLocalDisk 在磁盘中缓存的最大对象数(默认值为0,表示不限制) overflowToDisk 如果对象数量超过内存中最大的数,是否将其保存到磁盘中 eternal 缓存是否永远不过期(如果为false,还需要根据timeToIdleSeconds、timeToLiveSeconds判断) timeToIdleSeconds 对象的空闲时间(默认值为0秒,表示一直可以访问) timeToLiveSeconds 对象的存活时间(默认值为0秒,表示一直可以访问) 1、如果仅设置了timeToLiveSeconds,则该对象的超时时间=创建时间+timeToLiveSeconds,假设为A; 2、如果没设置timeToLiveSeconds,则该对象的超时时间=max(创建时间,最近访问时间)+timeToIdleSeconds,假设为B; 3、如果两者都设置了,则取出A、B最少的值,即min(A,B),表示只要有一个超时成立即算超时。 可选项 maxBytesLocalHeap 在内存中缓存的最大字节数(与maxEntriesLocalHeap属性不能同时指定,值可以加单位(K、M、G)) maxBytesLocalDisk 在磁盘中缓存的最大字节数(与maxEntriesLocalDisk属性不能同时指定,值可以加单位(CacheManager指定后可以加百分比) 指定后会隐式让当前cache的overflowToDisk为true) diskExpiryThreadIntervalSeconds 清理保存在磁盘上的过期缓存项目线程的启动时间间隔(默认值为120秒) diskSpoolBufferSizeMB 写入磁盘的缓冲区大小(默认为30MB,如果遇到OutOfMemory可以减小这个值) clearOnFlush Cache的flush()方法调用时,是否清空MemoryStore(默认为true) statistics 是否收集统计信息(默认为false,如果要监控缓存使用情况就开启,会影响性能) memoryStoreEvictionPolicy 当内存中缓存的对象数或字节数达到设定的上限时,如果overflowToDisk=false,就采用淘汰策略替换对象(默认为LRU,可选FIFO、LFU) 1、FIFO(first in first out 先进先出):淘汰最先进入的数据 2、LFU(Less Frequently Used 最少使用):淘汰最长时间没有被访问的数据 3、LRU(Least Recently Used 最近最少使用):淘汰一段时间内使用次数最少的数据 copyOnRead 当缓存被读出时,是否返回一份它的拷贝(默认为false) copyOnWrite 当缓存被写入时,是否写入一份它的拷贝(默认为false) --> <!--默认的缓存配置(可以给每个实体类指定一个对应的缓存,如果没有匹配到该类,则使用这个默认的缓存配置)--> <defaultCache maxEntriesLocalHeap="10000" maxEntriesLocalDisk="100000" overflowToDisk="true" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" /> <!-- <cache name="org.xs.techblog.modules.blog.entity.Daily" maxEntriesLocalHeap="1000" eternal="false" /> --> <!-- 指定缓存存放在磁盘上的位置 --> <diskStore path="java.io.tmpdir/demo1/ehcache/hibernate" /> </ehcache>
4、在实体类中增加1行@Cache注释
@Entity @Table(name="test") @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class testInfo {
其中CacheConcurrencyStrategy有5种并发性策略:
CacheConcurrencyStrategy.NONE 不使用缓存,默认的策略 CacheConcurrencyStrategy.READ_ONLY 只读模式,如果对数据更新了会报异常,适合不改动的数据 CacheConcurrencyStrategy.READ_WRITE 读写模式,更新缓存时会对缓存数据加锁,其他事务如果去取,发现被锁了,直接就去数据库查询 CacheConcurrencyStrategy.NONSTRICT_READ_WRITE 不严格的读写模式,更新缓存时不会加锁 CacheConcurrencyStrategy.TRANSACTIONAL 事务模式,支持回滚,当事务回滚时,缓存也能回滚
通常都是配置成只读模式的,读写模式的就具有事务隔离性了,而事务模式的事务隔离性最高。如果某些实体的数据经常修改、经常需要对缓存进行更新,性能就会变差,缓存也就失去了意义,这时就不如不用
5、增加相关方法、页面进行测试
testDao.java中增加方法:
public testInfo getInfo(String id) { return (testInfo) sessionFactory.getCurrentSession().get(testInfo.class, id); }
HelloController.java中增加方法:
@RequestMapping("list") public String list(HttpServletRequest request) { List<testInfo> list = testDao.getList(); request.setAttribute("testList", list); return "list"; } @RequestMapping("view/{id}") public String view(@PathVariable("id") String id, HttpServletRequest request) { testInfo info = testDao.getInfo(id); request.setAttribute("testInfo", info); return "view"; }
views中增加list.jsp、view.jsp页面:
list.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <title>Insert title here</title> <% /* 当前基础url地址 */ String path = request.getContextPath(); request.setAttribute("path", path); %> </head> <body> <c:if test="${!empty testList}"> <table border="1" width="100px"> <tr> <th>列1</th> <th>列2</th> </tr> <c:forEach items="${testList}" var="item"> <tr> <td>${item.id}</td> <td><a href="${path}/hello/view/${item.id}" target="_blank">${item.name}</a></td> </tr> </c:forEach> </table> </c:if> </body> </html>
view.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <title>Insert title here</title> </head> <body> <table border="1" width="100px"> <tr> <th>列1</th> <td>${testInfo.id}</td> </tr> <tr> <th>列2</th> <td>${testInfo.name}</td> </tr> </table> </body> </html>
(需要在pom.xml中增加jstl、standard,来开启对标签的支持,这里略)
运行测试,访问"http://localhost:8080/demo1/hello/list":
接着点击"666",弹出"http://localhost:8080/demo1/hello/view/2"页面:
Console信息没有变化,还是:
说明二级缓存生效了,第二次请求访问对象"666"的时候,已经是从之前缓存的数据里取了,没有再访问数据库
注:二级缓存缓存的是完整的对象,所以如果查询的是对象的某个属性,就不会添加添加到缓存里
二、查询缓存
二级缓存和查询缓存都是sessionFactory级别的,它们都相当于一个map,不同的是:
二级缓存的map是<对象id, 对象实体>的集合 查询缓存的map是<sql语句, 结果集合>的集合
二级缓存适用于单个对象重复使用的情况,不能缓存集合,如果是某个hql语句的结果集合要重复使用,就需要再开启查询缓存了(一般二级缓存都是和查询缓存搭配使用)。
在一次list查询后,查询缓存会将hql转换后的sql语句作为key,然后将查询的结果作为value缓存起来,下面是配置方法:
1、修改spring-context-hibernate.xml,在hibernateProperties里增加1行配置:
<property name="hibernateProperties"> <props> ... ... <!-- 开启查询缓存 --> <prop key="hibernate.cache.use_query_cache">true</prop> </props> </property>
在use_query_cache设置为true后,ehcache将会创建两个缓存区域:默认用StandardQueryCache保存查询结果集,UpdateTimestampsCache保存查询缓存的时间戳,所以可以在ehcache-hibernate.xml中增加这两项,属于可选配置:
<cache name="net.sf.hibernate.cache.StandardQueryCache" maxEntriesLocalHeap="10000" maxEntriesLocalDisk="100000" overflowToDisk="true" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" /> <cache name="net.sf.hibernate.cache.UpdateTimestampsCache" ... ... />
2、在Dao层的查询条件设置"setCacheable(true)"
String hql = "from testInfo"; Query query = sessionFactory.getCurrentSession().createQuery(hql); query.setCacheable(true); //开启查询缓存 return query.list();
有一个可选设置:
query.setCacheRegion("myCacheRegion"); //单独指定缓存名称,在ehcache-hibernate.xml中配置,替代StandardQueryCache
3、运行测试,访问"http://localhost:8080/demo1/hello/list":
Console信息:
之后这个地址重复刷新多次,Console信息中始终只有1条sql语句,说明查询缓存开启成功了
注:只有当hql查询语句完全相同、参数的值也完全相同时,查询缓存才有效,所以查询缓存的命中率是比较低的
当数据表中的任意数据发生一点修改时,整个表相关的查询缓存就失效了
(因为表数据修改后,时间戳更新,UpdateTimestampsCache里的时间戳不再是最新了,无法匹配所以缓存失效)
只有通过hibernate的hql修改数据才会刷新时间戳,如果直接使用sql或者使用其他应用程序修改数据库就无法监测到了
(因此query接口提供一个补救方法直接清除查询缓存:query.setForceCacheRefresh(true))
实例代码地址:https://github.com/ctxsdhy/cnblogs-example