前言
相比较Redis,Ehcache的配置和使用更为简单,它也是一个较为成熟的Java缓存框架,在一些简单的缓存应用场景下,使用ehcache完全可以搞定,就没必要只是为了使用Redis,而去搭建Redis服务,一位大牛说过:任何技术都是只是服务于业务的,不要用复杂代码来折磨自己,哈哈!当然这个视业务场景而言。
1.新建一个springboot项目,引入依赖如下:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.26</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--阿里巴巴json处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
<!-- Ehcache 依赖 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
2.配置文件application.properties
pb_url=127.0.0.1
#MYBATIS
mybatis.mapper-locations=classpath*:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.weige.ehcache01.entity
mybatis.configuration.cache-enabled=false
mybatis.configuration.map-underscore-to-camel-case=true
#Spring
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.validationQuery=SELECT 1
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
spring.mvc.view.prefix=classpath:/templates/
spring.mvc.view.suffix=.html
#server
server.port=8083
server.session-timeout=60
server.max-http-header-size=8192
#mysql
spring.datasource.url=jdbc:mysql://${pb_url}:3306/market_test?relaxAutoCommit=true&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&failOverReadOnly=false
spring.datasource.username=market_test
spring.datasource.password=sc1234567
#日志输出级别
logging.level.com.weige.ehcache01.mapper=debug
3.配置文件相同路径下,新建一个ehcache.xml文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="true" monitoring="autodetect"
dynamicConfig="true">
<diskStore path="java.io.tmpdir/kingpay_app"/>
<!-- <diskStore>==========当内存缓存中对象数量超过maxElementsInMemory时,将缓存对象写到磁盘缓存中(需对象实现序列化接口)
* <diskStore path="">==用来配置磁盘缓存使用的物理路径,Ehcache磁盘缓存使用的文件后缀名是*.data和*.index
* name=================缓存名称,cache的唯一标识(ehcache会把这个cache放到HashMap里)
* maxElementsOnDisk====磁盘缓存中最多可以存放的元素数量,0表示无穷大
* maxElementsInMemory==内存缓存中最多可以存放的元素数量,若放入Cache中的元素超过这个数值,则有以下两种情况
* 1)若overflowToDisk=true,则会将Cache中多出的元素放入磁盘文件中
* 2)若overflowToDisk=false,则根据memoryStoreEvictionPolicy策略替换Cache中原有的元素
* eternal==============缓存中对象是否永久有效,即是否永驻内存,true时将忽略timeToIdleSeconds和timeToLiveSeconds
* timeToIdleSeconds====缓存数据在失效前的允许闲置时间(单位:秒),仅当eternal=false时使用,默认值是0表示可闲置时间无穷大,此为可选属性
* 即访问这个cache中元素的最大间隔时间,若超过这个时间没有访问此Cache中的某个元素,那么此元素将被从Cache中清除
* timeToLiveSeconds====缓存数据在失效前的允许存活时间(单位:秒),仅当eternal=false时使用,默认值是0表示可存活时间无穷大
* 即Cache中的某元素从创建到清楚的生存时间,也就是说从创建开始计时,当超过这个时间时,此元素将从Cache中清除
* overflowToDisk=======内存不足时,是否启用磁盘缓存(即内存中对象数量达到maxElementsInMemory时,Ehcache会将对象写到磁盘中)
* 会根据标签中path值查找对应的属性值,写入磁盘的文件会放在path文件夹下,文件的名称是cache的名称,后缀名是data
* diskPersistent=======是否持久化磁盘缓存,当这个属性的值为true时,系统在初始化时会在磁盘中查找文件名为cache名称,后缀名为index的文件
* 这个文件中存放了已经持久化在磁盘中的cache的index,找到后会把cache加载到内存
* 要想把cache真正持久化到磁盘,写程序时注意执行net.sf.ehcache.Cache.put(Element element)后要调用flush()方法
* diskExpiryThreadIntervalSeconds==磁盘缓存的清理线程运行间隔,默认是120秒
* diskSpoolBufferSizeMB============设置DiskStore(磁盘缓存)的缓存区大小,默认是30MB
* memoryStoreEvictionPolicy========内存存储与释放策略,即达到maxElementsInMemory限制时,Ehcache会根据指定策略清理内存
* 共有三种策略,分别为LRU(最近最少使用)、LFU(最常用的)、FIFO(先进先出) -->
<!-- 注意,以下缓存是永久有效,是系统初始化数据到缓存中,如果不需要永久有效,请另写,或在 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
diskSpoolBufferSizeMB="30"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<!-- 缓存测试 -->
<cache name="randomCode"
maxEntriesLocalHeap="10000"
eternal="false"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
4.数据库新建一个测试表:test_qg,插入测试数据
4.根据测试表新建对应的实体类,mapper文件,在QgEntityMapper.xml文件中,我们新建一个查询所有信息的方法
<select id="getAllusrLvl" resultMap="BaseResultMap" >
select
gid, gname, gcont, gsts, remark
from test_qg
</select>
5.编写一个测试接口:GoodsController
package com.weige.ehcache01.controller;
import com.alibaba.fastjson.JSONObject;
import com.weige.ehcache01.entity.QgEntity;
import com.weige.ehcache01.entity.usrLvlEntity;
import com.weige.ehcache01.mapper.QgEntityMapper;
import com.weige.ehcache01.mapper.usrLvlEntityMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Controller
@Scope("prototype")
@RequestMapping("/")
public class GoodsController {
@Autowired
QgEntityMapper qgEntityMapper;
@Autowired
private CacheManager cacheManager;
@RequestMapping(value = "getAllgoods", method = RequestMethod.POST )
@ResponseBody
public String getAllusrLvl(HttpServletRequest request){
JSONObject jsonObject = new JSONObject();
// 获取ecache缓存对象
Cache lvlCache = cacheManager.getCache("randomCode");
List<QgEntity> QgEntitys= (List<QgEntity>) lvlCache.get("QgEntitys",List.class);
if (QgEntitys==null){
System.out.println("查询来到了数据库----->");
QgEntitys= qgEntityMapper.getAllusrLvl();
lvlCache.put("QgEntitys",QgEntitys);
}else {
System.out.println("缓存查到了数据");
}
jsonObject.put("QgEntitys",QgEntitys);
return JSONObject.toJSONString(jsonObject);
}
/**
*清空缓存
***/
@RequestMapping(value = "evictAllusrLvl", method = RequestMethod.POST )
@ResponseBody
public String evictCache(){
JSONObject jsonObject = new JSONObject();
// 获取ecache缓存对象
Cache lvlCache = cacheManager.getCache("randomCode");
List<QgEntity> usrLvlEntities= (List<QgEntity>) lvlCache.get("QgEntity",List.class);
if(usrLvlEntities!=null){
System.out.println("清空了缓存");
lvlCache.evict("QgEntitys");
jsonObject.put("resCode","00");
jsonObject.put("resMsg","清除成功");
}else {
jsonObject.put("resCode","01");
jsonObject.put("resMsg","暂无缓存可清除");
}
return JSONObject.toJSONString(jsonObject);
}
}
6.让项目跑起来,测试接口:getAllgoods
通过测试,我们可以发现:首次查询的时候,因为缓存中不存在目标数据,执行了SQL查询,我们再请求第二次的时候,并没有执行数据库查询,而是返回了数据,说明首次查询后,数据已被成功缓存,
当然这只是第一步,接下来我们模拟一下高并发下场景,如何使用缓存来减少请求对数据库的摧残:模拟N个用户同时对一个接口发起M次请求。
实现可以通过多线程,也可以使用工具,这边使用一个测试工具,下载地址:接口测压工具 下载之后解压,双击可执行文件
我们模拟五个用户同时发起请求,结果如下
可以看到,五次请求全部怼到了数据库上,缓存失效了。
改进一下代码,加一个重入锁ReentrantLock,改进后方法如下,这里我还加了双重检测:
public static Lock lock = new ReentrantLock();
@RequestMapping(value = "getAllgoods", method = RequestMethod.POST )
@ResponseBody
public String getAllusrLvl(HttpServletRequest request){
JSONObject jsonObject = new JSONObject();
// 获取ecache缓存对象
Cache lvlCache = cacheManager.getCache("randomCode");
List<QgEntity> QgEntitys= (List<QgEntity>) lvlCache.get("QgEntitys",List.class);
if (QgEntitys==null){//第一次检测缓存中不存在,此时需要同步方法
System.out.println("外层缓存没查到,锁住当前对象。。。。");
lock.lock();
try {
QgEntitys=(List<QgEntity>) lvlCache.get("QgEntitys",List.class);
if(QgEntitys==null){//第二次检测缓存中不存在,查询数据库
System.out.println("内层缓存未查到,查询来到了数据库----->");
QgEntitys= qgEntityMapper.getAllusrLvl();
lvlCache.put("QgEntitys",QgEntitys);
}else{
System.out.println("内层缓存查到数据。。。");
}
}catch ( Exception e){
}finally {
lock.unlock();
}
}else {
System.out.println("缓存查到了数据");
}
jsonObject.put("usrLvlEntities",QgEntitys);
return JSONObject.toJSONString(jsonObject);
}
模拟5个客户端同时请求:
控制台可以看到,只进行了一次数据库查询,通过缓存+锁的方式,减轻了数据库的访问压力
这块还有个问题,我开始是准备用synchronized 同步代码块来实现,但是发现并没有起作用,查询全部进了数据库,具体代码如下:
@RequestMapping(value = "getAllgoods", method = RequestMethod.POST )
@ResponseBody
public String getAllusrLvl(HttpServletRequest request){
JSONObject jsonObject = new JSONObject();
// 获取ecache缓存对象
Cache lvlCache = cacheManager.getCache("randomCode");
List<QgEntity> QgEntitys= (List<QgEntity>) lvlCache.get("QgEntitys",List.class);
if (QgEntitys==null){
System.out.println("缓存没查到---外层,锁住当前对象。。。。");
synchronized (this){
QgEntitys=(List<QgEntity>) lvlCache.get("QgEntitys",List.class);
if(QgEntitys==null){
System.out.println("查询来到了数据库----->");
QgEntitys= qgEntityMapper.getAllusrLvl();
lvlCache.put("QgEntitys",QgEntitys);
}else{
System.out.println("缓存没查到---内层");
}
}
}else {
System.out.println("缓存查到了数据");
}
jsonObject.put("usrLvlEntities",QgEntitys);
return JSONObject.toJSONString(jsonObject);
}
控制台日志:
具体我也没有找到原因,贴出来这块,希望能得到高人点拨一下。