Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)

       上一篇,我们已经讲过了,在Windows-64位系统下的redis3.0环境的搭建,其实很简单,就是一个解压缩文件的时间加上鼠标click几下的功夫就可以嗨皮的使用redis了,任何技术都是服务于应用的,没有应用场景,技术也敢叫技术?因此,本篇将结合上一篇,利用Spring-Boot框架,集成mybatis(数据操作用mybatis的通用mapper)+redis(数据缓存)来实现一个简单的中等数据量的查询且如何做到查询的优化以及减少数据库open session的开销。


本篇有点长,因为贴代码很占篇幅。


一、项目目录结构图


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第1张图片


demo资源会在文章最后,放在GitHub上



二、Pom依赖


pom.xml(默认是jar包,习惯性,我打成war包)



	4.0.0
	com.appleyk
	spring-boot-redis
	0.0.1-SNAPSHOT
	war
	spring-boot 集成redis,并利用AOP切面(切注解)的方式实现数据缓存操作
	
	
	
		org.springframework.boot
		spring-boot-starter-parent
		1.5.9.RELEASE
	
	
	
		1.8
	


	
		
		
		
		
		
		
			org.springframework.boot
			spring-boot-starter-web
		
		
		
			org.mybatis.spring.boot
			mybatis-spring-boot-starter
			1.1.1
		
		
		
			com.alibaba
			fastjson
			1.2.41
		
		
			com.fasterxml.jackson.core
			jackson-core
		
		
		
		
			mysql
			mysql-connector-java
		
		
		
		
		
		
			com.github.pagehelper
			pagehelper-spring-boot-starter
			1.1.1
		
		
		
			org.springframework.boot
			spring-boot-starter-test
			test
		
		
		
		
		
			org.springframework.boot
			spring-boot-devtools
			
			
			true
		
		
		
			junit
			junit
		
		
		
		
		
			tk.mybatis
			mapper-spring-boot-starter
			1.1.5
		
		
		
			org.springframework.boot
			spring-boot-starter-aop
		
		
			com.alibaba
			druid
			1.0.20
		
		
            org.springframework.boot
            spring-boot-starter-redis
            1.3.2.RELEASE
        
	
	
	
		
		
		   
			
				src/main/resources
			
			
				src/main/resources
				
					**/*.properties
					**/*.xml
				
				false
			
			
				src/main/java
				
					**/*.properties
					**/*.xml
				
				false
			
		
		
			
			
				org.springframework.boot
				spring-boot-maven-plugin
			
			
				org.apache.maven.plugins
				maven-surefire-plugin
				
					
						**/*Documentation.java
					
				
			
		
	



三、资源文件


(1)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第2张图片


(2)xxx.properties


application-dev.properties


#####开发环境

server.port=8081
server.session.timeout=10
server.tomcat.uri-encoding=utf8

#随机字符串
appleyk.name = ${random.value}
#随机整数
appleyk.age = ${random.int}
#10以内的随机数
appleyk.size = ${random.int(10)}

spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5

######MySql连接参数#############  
jdbc.url=jdbc\:mysql\://localhost\:3306/test?useUnicode\=true&autoReconnect=true&useSSL=false&characterEncoding\=utf-8&useSSL=true
jdbc.username=root
jdbc.password=root
jdbc.driverClassName=com.mysql.jdbc.Driver


application-prod.properties


#####生产环境


server.port=8088
server.session.timeout=10
server.tomcat.uri-encoding=utf8

#随机字符串
appleyk.name = ${random.value}
#随机整数
appleyk.age = ${random.int}
#10以内的随机数
appleyk.size = ${random.int(10)}

# 主数据源,默认的
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc\:mysql\://localhost\:3306/taotao?useUnicode\=true&autoReconnect=true&useSSL=false&characterEncoding\=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root

## Redis缓存
spring.cache.type=REDIS
spring.redis.database=0
spring.redis.host=localhost
spring.redis.password=
spring.redis.port=6379
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0  
spring.redis.pool.max-active=100 
spring.redis.pool.max-wait=-1
#是否开启redis缓存
spring.redis.cache.on = true


# 下面为连接池的补充设置,应用到上面所有数据源中  
# 初始化大小,最小,最大  
spring.datasource.initialSize=5  
spring.datasource.minIdle=5  
spring.datasource.maxActive=20  
# 配置获取连接等待超时的时间  
spring.datasource.maxWait=60000  
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒  
spring.datasource.timeBetweenEvictionRunsMillis=60000  
# 配置一个连接在池中最小生存的时间,单位是毫秒  
spring.datasource.minEvictableIdleTimeMillis=300000  
spring.datasource.testWhileIdle=true  
spring.datasource.testOnBorrow=true  
spring.datasource.testOnReturn=false  

#在application.properties文件中引入日志配置文件
#=====================================  log  =============================
logging.config=classpath:logback-boot.xml


application-test.properties


#####测试环境


server.port=8082
server.session.timeout=10
server.tomcat.uri-encoding=utf8

spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5

######MySql连接参数#############  
jdbc.url=jdbc\:mysql\://localhost\:3306/taotao?useUnicode\=true&autoReconnect=true&useSSL=false&characterEncoding\=utf-8&useSSL=true
jdbc.username=root
jdbc.password=root
jdbc.driverClassName=com.mysql.jdbc.Driver


application.properties


#SpringApplication将从以下位置加载application.properties文件, 并把它们添加到Spring Environment中:
#1. 当前目录下的一个/config子目录
#2. 当前目录
#3. 一个classpath下的/config包
#4. classpath根路径(root)
#这个列表是按优先级排序的(列表中位置高的将覆盖位置低的) 。
#注:你可以使用YAML('.yml') 文件替代'.properties'

#Spring-Boot多环境配置 -- prod:生产环境
spring.profiles.active = prod


logback-boot.xml(配置日志,控制台和日志记录文件的权限调成error级别,屏蔽掉info和debug信息)


    
        
    
    
        
            
            %d %p (%file:%line\)- %m%n  
            
            UTF-8   
            
        
    
    
    
        
         
        opt/spring-boot-web/logs/sys.log
        
        
            
            
            
            log/sys.%d.%i.log 
             
            30   
                
                  
                10MB    
                
            
            
            
                
                %d %p (%file:%line\)- %m%n  
                
            
            UTF-8    
            
        
    
    
    
          
      
    
    
     
         
            
     
   
  



四、annotation(缓存注解)


(1)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第3张图片


(2)CacheNameSpace.java


package com.appleyk.result;

/**
 * 缓存key的拼接前缀
 * @author [email protected]
 * @blob   http://blog.csdn.net/appleyk
 * @date   2018年3月1日-上午11:22:06
 */
public enum CacheNameSpace {
	ITEM
}


(2)DeleteCache.java


package com.appleyk.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.appleyk.result.CacheNameSpace;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeleteCache {
	CacheNameSpace nameSpace();
}



(3)QueryCache.java



package com.appleyk.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.appleyk.result.CacheNameSpace;


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface QueryCache{
	CacheNameSpace nameSpace();
}


(4)QueryCacheKey.java



package com.appleyk.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 注解 QueryCacheKey 是参数级别的注解,用来标注要查询数据的主键,会和上面的nameSpace组合做缓存的key值
 * @author [email protected]
 * @blob   http://blog.csdn.net/appleyk
 * @date   2018年2月28日-下午2:01:52
 */

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@Documented
public @interface QueryCacheKey {

}



关于缓存注解的使用,可以先看一张图(提前放送)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第4张图片


方法加了注解,接下来,如何进行数据缓存操作呢?  别急,AOP切面编程马上来帮忙





五、AOP(本篇精髓所在)



(1)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第5张图片


(2)先来看第一个,ControllerInterceptor.java


package com.appleyk.aop;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;


@Aspect
@Component
/**
 * 拦截器
 * @author [email protected]
 * @blob   http://blog.csdn.net/appleyk
 * @date   2018年3月1日-下午1:04:56
 * 开启对AspectJ自动代理技术
 * 
 * boolean proxyTargetClass() default false;

       描述:启用cglib代理,proxyTargetClass默认为false。

   boolean exposeProxy() default false;

       描述:如果在一个方法中,调用内部方法,需要在调用内部方法时也能够进行代理,比如内部调用时,使用

   (IService)AopContext.currentProxy().sayHello(),需要将exposeProx设置为true,默认为false。
 */
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
public class ControllerInterceptor {

	static Logger logger = LoggerFactory.getLogger(ControllerInterceptor.class);
    //ThreadLocal 维护变量 避免同步	
	//ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    ThreadLocal startTime = new ThreadLocal<>();// 开始时间
	
    /**
     * map1存放方法被调用的次数O
     */
    ThreadLocal> map1 = new ThreadLocal<>();
	
	 /**
     * map2存放方法总耗时
     */
	ThreadLocal> map2 = new ThreadLocal<>();
    
	
	/**
	 * 定义一个切入点. 解释下:
	 *
	 * ~ 第一个 * 代表任意修饰符及任意返回值. ~ 第二个 * 定义在web包或者子包 ~ 第三个 * 任意方法 ~ .. 匹配任意数量的参数.
	 */
	static final String pCutStr = "execution(* com.appleyk.*..*(..))";

	@Pointcut(value = pCutStr)
	public void logPointcut() {
	}

	@Around("logPointcut()")
	public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

		
		//初始化 一次
		if(map1.get() ==null ){
			map1.set(new HashMap<>());
			
		}
		
		if(map2.get() == null){
			map2.set(new HashMap<>());
		}
		
		long start = System.currentTimeMillis();
		try {
			Object result = joinPoint.proceed();
		
			long end = System.currentTimeMillis();

			logger.info("===================");
			String tragetClassName = joinPoint.getSignature().getDeclaringTypeName();
			String MethodName = joinPoint.getSignature().getName();
									
			Object[] args = joinPoint.getArgs();// 参数
			int argsSize = args.length;
			String argsTypes = "";
			String typeStr = joinPoint.getSignature().getDeclaringType().toString().split(" ")[0];
			String returnType = joinPoint.getSignature().toString().split(" ")[0];
			logger.info("类/接口:" + tragetClassName + "(" + typeStr + ")");
			logger.info("方法:" + MethodName);
			logger.info("参数个数:" + argsSize);
			logger.info("返回类型:" + returnType);
			if (argsSize > 0) {
				// 拿到参数的类型
				for (Object object : args) {
					argsTypes += object.getClass().getTypeName().toString() + " ";
				}
				logger.info("参数类型:" + argsTypes);
			}

			Long total = end - start;
			logger.info("耗时: " + total + " ms!");
					
			if(map1.get().containsKey(MethodName)){
				Long count = map1.get().get(MethodName);
				map1.get().remove(MethodName);//先移除,在增加
				map1.get().put(MethodName, count+1);
				
				count = map2.get().get(MethodName);
				map2.get().remove(MethodName);
				map2.get().put(MethodName, count+total);
			}else{
				
				map1.get().put(MethodName, 1L);
				map2.get().put(MethodName, total);
			}
			
			return result;

		} catch (Throwable e) {
			long end = System.currentTimeMillis();
			logger.info("====around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : "
					+ e.getMessage());
			throw e;
		}

	}

	//对Controller下面的方法执行前进行切入,初始化开始时间
	@Before(value = "execution(* com.appleyk.controller.*.*(..))")
	public void beforMehhod(JoinPoint jp) {
		startTime.set(System.currentTimeMillis());
	}

	//对Controller下面的方法执行后进行切入,统计方法执行的次数和耗时情况
	//注意,这里的执行方法统计的数据不止包含Controller下面的方法,也包括环绕切入的所有方法的统计信息
	@AfterReturning(value = "execution(* com.appleyk.controller.*.*(..))")
	public void afterMehhod(JoinPoint jp) {
		long end = System.currentTimeMillis();
		long total =  end - startTime.get();
		String methodName = jp.getSignature().getName();
		logger.info("连接点方法为:" + methodName + ",执行总耗时为:" +total+"ms");
		
		//重新new一个map
		Map map = new HashMap<>();
		
		//从map2中将最后的 连接点方法给移除了,替换成最终的,避免连接点方法多次进行叠加计算	
		//由于map2受ThreadLocal的保护,这里不支持remove,因此,需要单开一个map进行数据交接
		for(Map.Entry entry:map2.get().entrySet()){
			if(entry.getKey().equals(methodName)){				
				map.put(methodName, total);
				
			}else{
				map.put(entry.getKey(), entry.getValue());
			}			
		}
	
		for (Map.Entry entry :map1.get().entrySet()) {
			for(Map.Entry entry2 :map.entrySet()){
				if(entry.getKey().equals(entry2.getKey())){
					System.err.println(entry.getKey()+",被调用次数:"+entry.getValue()+",综合耗时:"+entry2.getValue()+"ms");
				}
			}
			
		}
		
		System.err.println("=======================方法执行效率统计切面结束");
	}


}


以上功能有两个,一个是环绕切面,根据切点表达式:execution(* com.appleyk.*..*(..))我们可以看出来,方法



Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第6张图片


比如,我们放开日志的权限,调成info级别如下


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第7张图片



当我们启动Spring-Boot项目的时候,我们的第一个切面方法doAround就起作用了(方法分析统计),效果如下:


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第8张图片


如果我们把logback的日志输出级别调成:error


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第9张图片


则我们关掉项目,再次启动的时候,就看不到之前上面的切面输出的效果了(因为error级别比info高,info不会出现)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第10张图片


以上说明,logback日志权限级别不是乱设置的,是有讲究的,哈哈,我们继续


上面是第一个切点的编程,第二是什么呢?


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第11张图片



上述图中注释说明的很清楚了,两个注解,一个是前置通知@Before,在方法调用前,记录开始时间,一个是方法执行后的通知@AfterReturning,这个实现方法的效率统计,比如xxx方法总共被调用了多少次、耗费了多少时间(毫秒),效果如下:






我们继续看另一个AOP编程,本篇的重点


(3)RedisCacheAspect.java



package com.appleyk.aop;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.appleyk.annotation.DeleteCache;
import com.appleyk.annotation.QueryCache;
import com.appleyk.annotation.QueryCacheKey;
import com.appleyk.result.CacheNameSpace;

@Aspect
@Component

/**
 * 利用AOP配合注解,实现redis缓存的写入和删除
 * @author [email protected]
 * @blob   http://blog.csdn.net/appleyk
 * @date   2018年3月1日-下午1:35:30
 */
public class RedisCacheAspect {

	static Logger logger = LoggerFactory.getLogger(ControllerInterceptor.class);

	@Autowired
	private RedisTemplate redisTemplate;

	/**
	 * 是否开启redis缓存,将查询的结果写入value
	 */
	@Value("${spring.redis.cache.on}")
	private boolean isOn;

	/**
	 * 定义拦截规则:拦截所有@QueryCache注解的方法 -- 查询。
	 */
	@Pointcut("@annotation(com.appleyk.annotation.QueryCache)")
	public void queryCachePointcut() {
	}

	/**
	 * 拦截器具体实现
	 * 
	 * @param point
	 * @return
	 * @throws Throwable
	 */
	@Around("queryCachePointcut()")
	public Object InterceptorByQuery(ProceedingJoinPoint point) throws Throwable {
		long beginTime = System.currentTimeMillis();
		if (!isOn) {
			// 如果不开启redis缓存的话,直接走原方法进行查询
			Object object = point.proceed();
			return object;
		}

		System.err.println("AOP 缓存切面处理 >>>> start ");
		MethodSignature signature = (MethodSignature) point.getSignature();
		Method method = signature.getMethod(); // 获取被拦截的方法
		// 拿到方法上标注的注解的namespace的值
		CacheNameSpace cacheType = method.getAnnotation(QueryCache.class).nameSpace();
		String key = null;
		int i = 0;

		// 循环所有的参数
		for (Object value : point.getArgs()) {

			MethodParameter methodParam = new SynthesizingMethodParameter(method, i);
			Annotation[] paramAnns = methodParam.getParameterAnnotations();
			// 循环参数上所有的注解
			for (Annotation paramAnn : paramAnns) {
				if (paramAnn instanceof QueryCacheKey) { //
					QueryCacheKey requestParam = (QueryCacheKey) paramAnn;
					key = cacheType.name() + "_" + value; // 取到QueryCacheKey的标识参数的值
				}
			}
			i++;
		}

		/**
		 * 如果没有参数的话,设置Key值
		 */
		if (key == null) {
			key = cacheType.name();
		}

		// 获取不到key值,抛异常
		if (StringUtils.isEmpty(key))
			throw new Exception("缓存key值不存在");

		ValueOperations operations = redisTemplate.opsForValue();
		boolean hasKey = redisTemplate.hasKey(key);
		if (hasKey) {

			// 缓存中获取到数据,直接返回。
			Object object = operations.get(key);
			System.err.println("本次查询缓存命中,从缓存中获取到数据 >>>> key = " + key);
			System.err.println("AOP 缓存切面处理 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
			return object;
		}

		// 缓存中没有数据,调用原始方法查询数据库
		Object object = point.proceed();

		operations.set(key, object, 30, TimeUnit.SECONDS); // 设置超时时间30s

		System.err.println("本次查询缓存未命中,DB取到数据并存入缓存 >>>> key =" + key);
		System.err.println("AOP 缓存切面处理 >>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
		// redisTemplate.delete(key);
		return object;

	}

	/**
	 * 定义拦截规则:拦截所有@DeleteCache注解的方法 -- 用于修改表数据时,删除redis缓存中的key值。
	 * 也可以使用切面表达式execution(* com.appleyk.*..*(..)) 切和更新数据相关的方法
	 */
	@Pointcut("@annotation(com.appleyk.annotation.DeleteCache)")
	public void deleteCachePointcut() {
	}

	/**
	 * 拦截器具体实现
	 * 
	 * @param point
	 * @return
	 * @throws Throwable
	 */
	@Around("deleteCachePointcut()")
	public Object InterceptorBySave(ProceedingJoinPoint point) throws Throwable {

		long beginTime = System.currentTimeMillis();
		if (!isOn) {
			// 如果不开启redis缓存的话,直接走原方法进行查询
			Object object = point.proceed();
			return object;
		}
		System.err.println("AOP 缓存切面处理 【清除key】>>>> start ");
		MethodSignature signature = (MethodSignature) point.getSignature();
		Method method = signature.getMethod(); // 获取被拦截的方法

		// 拿到方法上标注的注解的namespace的值
		CacheNameSpace cacheType = method.getAnnotation(DeleteCache.class).nameSpace();

		String key = cacheType.name();
		ValueOperations operations = redisTemplate.opsForValue();
		boolean hasKey = redisTemplate.hasKey(key);
		if (hasKey) {

			System.err.println("key存在,执行删除 >>>> key = " + key);
			/**
			 * 删除key
			 */
			redisTemplate.delete(key);
		}
		System.err.println("AOP 缓存切面处理 【清除key】>>>> end 耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
		Object object = point.proceed();
		return object;

	}

}


注意以下几点


A.

RedisTemplate -- spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。





这里我们可以大胆的使用@Autowired注入我们需要的RedisTemplate 对象,是因为我们在Pom文件中依赖了如下



Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第12张图片


而Spring-Boot启动的时候,也会将redis相关的资源添加到Spring容器中(至于资源是否存在,还有待验证)


我们可以看一下spring-boot-starter-redis的依赖树,如下


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第13张图片




B.


#是否开启redis缓存
spring.redis.cache.on = true


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第14张图片



C. 

redis缓存key的生成策略(当然下面是简单的组成,实际应用中,key的值要比下面的复杂的多,本篇知道就行)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第15张图片


D. 


缓存命中与否


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第16张图片


E. 


删除缓存(保证,在修改数据的时候,重刷相应redis缓存,避免缓存数据和实际DB数据不同步)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第17张图片



说完AOP,我们在来说一下,本篇的数据源配置



六、Mybatis数据源配置(Bean注入)



(1)






(2)MybatisConfig.java


package com.appleyk.config;

import java.util.Properties;

import javax.sql.DataSource;

import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.github.pagehelper.PageInterceptor;

@Configuration
@EnableTransactionManagement//开启事务
@EnableConfigurationProperties(DataSourceProperties.class)
//扫描一切和Mapper有关的bean,因此,下面对整个项目进行"全身"扫描
@MapperScan("com.appleyk")
public class MybatisConfig {

	
	@Bean(name = "dataSource")
	//Spring 允许我们通过 @Qualifier注释指定注入 Bean 的名称
    @Qualifier(value = "dataSource")
    @ConfigurationProperties(prefix="spring.datasource")
	@Primary
    public DataSource dataSource()
    {
		return DataSourceBuilder.create().build();
    }

	//创建SqlSessionFactory
	@Bean(name = "sqlSessionFactory")
	public SqlSessionFactory sqlSessionFactoryBean(@Qualifier("dataSource") DataSource dataSource){
		
		SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
	
		//1.设置数据源
		bean.setDataSource(dataSource);
		//2.给包中的类注册别名,注册后可以直接使用类名,而不用使用全限定的类名(就是不用包含包名)
		bean.setTypeAliasesPackage("com.appleyk.database");
			
		// 设置MyBatis分页插件 【PageHelper 5.0.1设置方法】
        PageInterceptor pageInterceptor = new PageInterceptor();
        Properties properties = new Properties();
        properties.setProperty("helperDialect", "mysql");
        properties.setProperty("offsetAsPageNum", "true");
        properties.setProperty("rowBoundsWithCount", "true");
        pageInterceptor.setProperties(properties);
        
        //添加插件
        bean.setPlugins(new Interceptor[]{pageInterceptor});

        //添加XML目录,进行Mapper.xml扫描
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try {
        	//项目中的xxxMapper.xml位于包com.appleyk.mapepr下面
            bean.setMapperLocations(resolver.getResources("classpath*:com/appleyk/mapepr/*.xml"));        
            return bean.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }       
	} 
	
	//创建SqlSessionTemplate
	@Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
		
	
	@Bean(name = "transactionManager")
    @Primary
    public DataSourceTransactionManager testTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }	
}

这个没什么好说的,不懂的地方,留言备注



七、MySql数据示例



(1)使用taotao数据库里面的tb_item表里的数据作为本篇的查询依据


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第18张图片


(2)没有的,可以自己新建一个表,批量插入XXX条数据,扩展自己的查询数据集





八、使用Mybatis通用mapper完成DAO层的设计和编写



(1)依赖的包,在Pom文件中体现如下:


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第19张图片



(2)tb_item表Java实体类映射


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第20张图片


Tbitem.java


package com.appleyk.entity;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;

import org.apache.ibatis.type.JdbcType;

import tk.mybatis.mapper.annotation.ColumnType;

@Table(name = "tb_item")
public class TbItem implements Serializable{

	@Id
	@Column(name = "id")
	@ColumnType(jdbcType = JdbcType.BIGINT)
	private Long id;
	private String title;
	private String sell_point;
	private Long price;
	private Integer num;
	private String barcode;
	private String image;
	private Integer cid;
	private Integer status;
	private Date created;
	private Date updated;

	
	public TbItem(){
		
	}
	
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getSell_point() {
		return sell_point;
	}

	public void setSell_point(String sell_point) {
		this.sell_point = sell_point;
	}

	public Long getPrice() {
		return price;
	}

	public void setPrice(Long price) {
		this.price = price;
	}

	public Integer getNum() {
		return num;
	}

	public void setNum(Integer num) {
		this.num = num;
	}

	public String getBarcode() {
		return barcode;
	}

	public void setBarcode(String barcode) {
		this.barcode = barcode;
	}

	public String getImage() {
		return image;
	}

	public void setImage(String image) {
		this.image = image;
	}

	public Integer getCid() {
		return cid;
	}

	public void setCid(Integer cid) {
		this.cid = cid;
	}

	public Integer getStatus() {
		return status;
	}

	public void setStatus(Integer status) {
		this.status = status;
	}

	public Date getCreated() {
		return created;
	}

	public void setCreated(Date created) {
		this.created = created;
	}

	public Date getUpdated() {
		return updated;
	}

	public void setUpdated(Date updated) {
		this.updated = updated;
	}

}

需要注意的地方


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第21张图片



其他的比葫芦画瓢,可以扩展自己的实体类,



(3)tb_item实体类对应的mapper操作


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第22张图片


TbItemMapper.java


package com.appleyk.mapper;

import com.appleyk.entity.TbItem;

import tk.mybatis.mapper.common.Mapper;

public interface TbItemMapper extends Mapper{

}


什么叫通用mapper,也就是帮我们省去了基本的增删改查语句,无需配置mapepr.xml,无需我们写一句代码,mybatis就可以帮助我们实现tb_item这个表的简单数据操作(其实也不能说是简单,因为单表操作也就那回事,)


有了DAO层,接下来,我们就需要借助Service层来调用了





九、Service层(缓存注解的使用)


(1)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第23张图片


(2)TbItemService.java


package com.appleyk.service;

import java.util.List;

import com.appleyk.entity.TbItem;

public interface TbItemService {
    
	/**
	 * 查询全部商品
	 * @return
	 */
	List GetTbItems();
	
	/**
	 * 根据商品ID查询
	 * @param id
	 * @return
	 */
	TbItem GetTbItem(Long id);
	
	/**
	 * 保存商品
	 * @param tbItem
	 * @return
	 */
	boolean     SaveTbItems(TbItem tbItem);
}


(3)TbItemServiceImpl.java


package com.appleyk.service.Impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import com.appleyk.annotation.DeleteCache;
import com.appleyk.annotation.QueryCache;
import com.appleyk.entity.TbItem;
import com.appleyk.mapper.TbItemMapper;
import com.appleyk.result.CacheNameSpace;
import com.appleyk.service.TbItemService;

import tk.mybatis.mapper.entity.Example;

@Service
@Primary
public class TbItemServiceImpl implements TbItemService {

	@Autowired
	private TbItemMapper tbItemMapper ;

	/**
	 * 
	 * 获取商品: 如果缓存存在,从缓存中获取商品信息 如果缓存不存在,从DB中获取商品信息,然后插入缓存
	 * 
	 */
	@Override
	@QueryCache(nameSpace = CacheNameSpace.ITEM)
	public List GetTbItems() {

		Example example = new Example(TbItem.class);

		List tbItems = tbItemMapper.selectByExample(example);
		return tbItems;
	}

	@Override
	public TbItem GetTbItem(Long id) {
		//直接根据主键返回商品实体
		return tbItemMapper.selectByPrimaryKey(id);
	}

	@Override
	@DeleteCache(nameSpace = CacheNameSpace.ITEM)
	public boolean SaveTbItems(TbItem tbItem) {
		/**
		 * 这里不做操作,只是模拟,到这一步的时候,切面执行删除查询的时候写入缓存中的key
		 */
	
		return true;
	}

}



这个没什么好说的,就是两个查询,和一个没有实现保存效果的方法(写代码很累的.....)


但是别小看他们,他们可是加了缓存注解的,你要知道,我们上面再讲AOP的时候,可提到过,有一个AOP编程切的就是带有这个缓存注解的方法,从而实现redis缓存操作的,不信,我们继续往下走,Service层有了,该轮到我们的Controller层了



十、Controller层(提供Restful API风格的接口)



(1)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第24张图片



(1)TbItemController.java


package com.appleyk.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.appleyk.entity.TbItem;
import com.appleyk.result.ResponseMessage;
import com.appleyk.result.ResponseResult;
import com.appleyk.service.TbItemService;

@RestController
@RequestMapping("/rest/v1.0.1/database/tbitem")
public class TbItemController {

	@Autowired
	private TbItemService itemService;

	@GetMapping("/query")
	public ResponseResult GetTbItems() {
		List result = itemService.GetTbItems();
		return new ResponseResult(200, "查询成功,size = " + result.size(), result);
	}

	@PostMapping("/save")
	public ResponseResult SaveTbItem() {
		TbItem tbItem = new TbItem();
		if (itemService.SaveTbItems(tbItem)) {
			return new ResponseResult(ResponseMessage.OK);
		}

		return new ResponseResult(ResponseMessage.INTERNAL_SERVER_ERROR);

	}
}

这个也没有上面好说的,万事俱备,只欠东风!


接下来,我们实际演示一下,走个调用



十一、Spring-Boot启动(run)



(1)前提一定要保证,本机的redis-server是开着的



Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第25张图片


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第26张图片




十二、API专业测试工具Insomnia的使用



(1)先来个查询的


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第27张图片


第一次查询,我们后台AOP切入的结果输出:


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第28张图片


第二次查询,我们后台AOP切入的结果输出:


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第29张图片


第三次查询,我们后台AOP切入的结果输出:



Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第30张图片




如果我们在第一次查询数据的时候,紧接着来了一个保存商品信息的操作,会看到如下效果输出(具体调用不在放出)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第31张图片



十三、关闭缓存支持


(1)


Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第32张图片



(2)效果如下



Spring-Boot 集成Redis实现查询缓存提高查询效率减轻数据库访问压力(涉及key的添加和删除)_第33张图片




项目GitHUb资源链接:https://github.com/kobeyk/appleyk-spring-boot.git

你可能感兴趣的:(Spring-Boot)