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

 

posted @ 2017-02-15 19:56  syxsdhy  阅读(328)  评论(0编辑  收藏  举报