springboot整合ehcache(含高并发测试)

前言
相比较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);
    }

控制台日志:
在这里插入图片描述
具体我也没有找到原因,贴出来这块,希望能得到高人点拨一下

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值