Redis缓存机制
二、springboot整合Redis
1.Redis入门案例
1.1导入jar包
<!--spring整合redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
1.2客户端操作Redis String类型命令 Test测试
package com.jt.test;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
//@SpringBootTest //目的:动态获取spring容器中的数据
public class TestRedis {
/**
* 主要目的测试程序远程操作Redis是否有效
* 配置redis服务:
* 1.redis需要关闭IP绑定模式
* 2.redis关闭保护模式
* 3.redis最好开启后端运行
*
* 完成redis客户端操作
*/
@Test
public void test01() throws InterruptedException {
//1.测试链接
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.set("a", "动态获取redis中的数据");
System.out.println(jedis.get("a"));
//2.测试数据是否存在
if(jedis.exists("a")){
jedis.set("a", "修改数据");
}else{
jedis.set("a", "新增数据");
}
//3.删除redis
jedis.del("a");
//4.清空所有的数据
jedis.flushDB();
jedis.flushAll();
//5.为数据添加超时时间
jedis.set("b", "设定超时时间");
jedis.expire("b", 10);
Thread.sleep(2000);
System.out.println(jedis.ttl("b"));
}
//原子性
@Test
public void test02(){
Jedis jedis = new Jedis("192.168.126.129", 6379);
jedis.set("c", "测试redis");
//需求1: 如果数据不存在时,才会为数据赋值.
jedis.setnx("d","测试setnx方法");
System.out.println(jedis.get("d"));
//需求2: 需要为数据添加超时时间,同时满足原子性的要求
//jedis.set("s", "为数据添加超时时间");
//有时程序中断了,下列的方法将不会执行.
//jedis.expire("s", 20);
//System.out.println(jedis.ttl("s"));
//为数据添加超时时间
jedis.setex("s", 20, "为数据添加超时111");
System.out.println("获取超时时间:"+jedis.ttl("s"));
}
/**
* 需求: 如果数据存在才修改,并且为数据添加超时时间,满足原子性要求
* SetParams:
* XX: 数据存在时赋值.
* NX: 数据不存在时赋值
* EX: 添加超时时间单位秒
* PX: 添加超时时间单位毫秒
*/
@Test
public void test03(){
Jedis jedis = new Jedis("192.168.126.129", 6379);
jedis.flushAll();
SetParams setParams = new SetParams();
setParams.xx().ex(20);
jedis.set("a", "测试方法",setParams);
System.out.println(jedis.get("a"));
}
}
1.3关于List集合说明
1.3.1关于队列应用场景
秒杀场景: 马上过年了, 店铺周年店庆 1部苹果12proMax 12000 1元秒杀? 提前预付活动费 10块… 如果秒杀不成功 则7日内退还?
1.3.2入门案例测试
@Test
public void testList(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.lpush("list", "1","2","3");
System.out.println(jedis.rpop("list")); //队列
}
1.4关于事物控制
//弱事务控制
@Test
public void testTx(){
Jedis jedis = new Jedis("192.168.126.129",6379);
Transaction transaction = jedis.multi(); //开启事务
try {
transaction.set("k", "k");
transaction.set("c", "c");
transaction.exec();
}catch (Exception e){
e.printStackTrace();
transaction.discard();
}
}
2.SpringBoot整合Redis
2.1编辑pro配置文件
说明:由于redis是公共的第三方,所以将配置放到jt-common中即可
在jt-common的resources目录下创建redis.properties配置文件
2.2编辑配置类
说明: 需要在jt-common中添加redis的配置类
package com.jt.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.Jedis;
@Configuration //表示一个配置类 一般会与@Bean的注解联用
@PropertySource("classpath:/redis.properties") //导入配置文件
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private Integer port;
@Bean //将方法的返回值结果,交给spring容器进行管理.
public Jedis jedis(){
return new Jedis(host, port);
}
}
2.3测试redis案例
说明:springboot整合redis测试写在test中。
package com.jt.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.params.SetParams;
@SpringBootTest //目的:动态获取spring容器中的数据
public class TestRedis {
@Autowired
private Jedis jedis;
@Test
public void testSpringJedis(){
jedis.set("jedis", "spring对象测试");
System.out.println(jedis.get("jedis"));
}
3.JSON转化工具API
3.1入门案例测试
package com.jt.test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.pojo.ItemDesc;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class TestObjectMapper {
@Test
public void test01() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
//将对象转化为JSON 调用的是对象的get方法获取属性/属性的值
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(1000L).setItemDesc("对象与json转化")
.setCreated(new Date()).setUpdated(new Date());
String json = objectMapper.writeValueAsString(itemDesc);
System.out.println(json);
//将JSON串转化为对象 调用的是对象的set方法为对象属性赋值
ItemDesc itemDesc2 = objectMapper.readValue(json, ItemDesc.class);
System.out.println(itemDesc2.getItemDesc());
}
@Test
public void test02() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
//将对象转化为JSON 调用的是对象的get方法获取属性/属性的值
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(1000L).setItemDesc("对象与json转化").setCreated(new Date()).setUpdated(new Date());
ItemDesc itemDesc2 = new ItemDesc();
itemDesc2.setItemId(2000L).setItemDesc("对象与json转化2").setCreated(new Date()).setUpdated(new Date());
List<ItemDesc> list2 = new ArrayList<>();
list2.add(itemDesc);
list2.add(itemDesc2);
String json = objectMapper.writeValueAsString(list2);
System.out.println(json);
//将JSON串转化为对象 调用的是对象的set方法为对象属性赋值
List list3 = objectMapper.readValue(json,list2.getClass());
System.out.println(list3);
}
}
3.2封装工具API
package com.jt.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.pojo.Item;
import com.jt.pojo.ItemDesc;
import com.sun.corba.se.spi.ior.IORTemplate;
/**
* 该工具类,主要的功能实现对象与JSON串的互相转化.
* 1.对象转化为JSON
* 2.JSON转化为对象
*/
public class ObjectMapperUtil {
private static final ObjectMapper MAPPER = new ObjectMapper();
//1.对象转化为JSON
public static String toJSON(Object object){
try {
return MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
//2.JSON转化为对象 要求用户传递什么类型就返回什么对象??
public static <T> T toObj(String json,Class<T> target){
try {
return MAPPER.readValue(json, target);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
4.利用缓存实现商品分类查询
4.1业务说明
说明: 商品分类信息每次展开封闭的节点,都需要查询数据库.这样的效率并不高. 可以使用redis缓存来提升效率.
流程:
①用户第一次查询先查询缓存
②缓存中没有数据(这就是第一次查询),查询数据库. 将数据库记录保存到缓存中即可.
③缓存中有记录. 直接通过缓存获取数据之后返回即可.
4.2编辑ItemCatController
/**
* 业务: 实现商品分类的查询
* URL地址: http://localhost:8091/itemCat/list?id=xxx
* 请求参数: 传递节点的ID
* 返回值: List<EasyUITree>对象 页面JS-VO~~~~POJO--DB
*/
@RequestMapping("/list")
public List<EasyUITree> findItemCatList(Long id){
//1.查询一级商品分类信息
Long parentId = (id==null?0L:id);
//return itemCatService.findItemCatList(parentId);
//利用redis缓存查询数据
return itemCatService.findItemCatCache(parentId);
}
4.3编辑ItemCatService
/**
* 原理说明:
* 1.定义存取redis中的key 业务名称+标识符 ITEMCAT_PARENTID::0
* 2.通过key获取redis中的记录
* 3.空: 查询数据库 将返回值结果保存到缓存中即可
* 4.非空 直接将缓存数据获取之后,返回给用户即可.
* @param parentId
* @return
*/
@Override
public List<EasyUITree> findItemCatCache(Long parentId) {
long startTime = System.currentTimeMillis();
String key = "ITEMCAT_PARENTID::" + parentId;
List treeList = new ArrayList();
if(jedis.exists(key)){
//如果存在则直接返回
String json = jedis.get(key);
treeList = ObjectMapperUtil.toObj(json, treeList.getClass());
System.out.println("查询Redis缓存!!!");
long endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime - startTime)+"毫秒");
}else{
//如果不存在 则查询数据库.
treeList = findItemCatList(parentId);
//将数据保存到缓存中
String json = ObjectMapperUtil.toJSON(treeList);
jedis.set(key,json);
System.out.println("查询数据库!!!");
long endTime = System.currentTimeMillis();
System.out.println("耗时:"+(endTime - startTime)+"毫秒");
}
return treeList;
}
4.4速度差
5.利用AOP实现商品分类缓存
5.1为什么使用AOP实现商品分类缓存
问题1: 如果将业务代码直接写死,那么该代码不具有通用性.
问题2: 代码冗余 代码的耦合性高.
AOP: 面向切面编程.
AOP作用: 在不修改原有方法的条件下.对原有的方法进行扩展.
5.2AOP复习
公式: AOP = 切入点表达式 + 通知方法
5.2.1通知方法
①before 目标方法执行之前执行
②afterThrowing 目标方法执行之后 抛出异常时执行
③afterReturning 目标方法执行之后 返回结果时执行
④after 目标方法执行之后执行(finally)
⑤around 环绕通知功能最为强大 可以控制目标方法的执行 在目标方法执行前后都要执行
5.2.2切入点表达式
①bean(bean的Id) 按照bean匹配!! Spring容器管理的对象称之为bean 粗粒度
②within(包名.类名) 按照包路径匹配 其中可以使用通配符代替
within("com.jt.service. ") 位于com.jt.service中的包的所有的类都会匹配. 粗粒度
③execution(返回值类型 包名.类名.方法名(参数列表)) 匹配的是方法参数级别 细粒度
execution(* com.jt.service…(…)) 解释:返回值类型任意 在com.jt.service的包路径中的任意类的任意方法的任意参数…
execution(* com.jt.service.userService.add*(int,String))
④@annotation(包名.注解名称) 按照注解匹配.
注解: @Find
@annotation(com.jt.anno.Find)
5.2.3关于AOP案例
package com.jt.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import java.util.Arrays;
/*@Service
@Controller
@Repository*/
@Component //组件 将类交给spring容器管理
@Aspect //表示我是一个切面
public class RedisAOP {
//公式 aop = 切入点表达式 + 通知方法
//@Pointcut("bean(itemCatServiceImpl)")
//@Pointcut("within(com.jt.service.*)")
//@Pointcut("execution(* com.jt.service.*.*(..))") //.* 当前包的一级子目录
@Pointcut("execution(* com.jt.service..*.*(..))") //..* 当前包的所有的子目录
public void pointCut(){
}
//如何获取目标对象的相关参数?
//ProceedingJoinPoint is only supported for around advice
@Before("pointCut()")
public void before(JoinPoint joinPoint){ //连接点
Object target = joinPoint.getTarget();
Object[] args = joinPoint.getArgs();
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
System.out.println("目标对象:"+target);
System.out.println("方法参数:"+Arrays.toString(args));
System.out.println("类名称:"+className);
System.out.println("方法名称:"+methodName);
}
//作业: 利用自定义注解@CacheFind 实现缓存查询!!!!
}
5.3AOP实现缓存业务
5.3.1业务需求
(1)自定义注解 @CacheFind(key=“xxx”,second=-1)。
(2) 使用自定义注解 标识业务方法 将方法的返回值保存到缓存中。
(3) 利用AOP 拦截注解 利用环绕通知方法实现业务。
5.3.2自定义注解@CacheFind
package com.jt.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) //此注解在方法中使用
@Retention(RetentionPolicy.RUNTIME) //运行期有效
public @interface CacheFind {
String key();//定义业务key
int seconds() default -1;//定义超时时间 -1表示不超时
}
5.3.3注解标识
说明:在业务层需要使用redis缓存的业务上加@CacheFind注解
/**
* 核心问题: 将POJO对象转化为VO对象
* @param parentId
* @return
*/
@Override
@CacheFind(key = "ITEMCAT_PARENTID")//标识业务名称,一般都是大写的
public List<EasyUITree> findItemCatList(Long parentId) {
//2.定义VO的返回值
List<EasyUITree> treeList = new ArrayList<>();
5.3.4编辑AOP
package com.jt.aop;
import com.jt.anno.CacheFind;
import com.jt.util.ObjectMapperUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Method;
import java.util.Arrays;
/*@Service
@Controller
@Repository*/
@Component //组件 将类交给spring容器管理
@Aspect //表示我是一个切面
public class RedisAOP {
@Autowired
private Jedis jedis;
/*
* 实现AOP业务调用
* 1.拦截指定的注解
* 2.利用环绕通知实现
* 实现步骤:
* 1.获取KEY 必须先获取注解 从注解中获取key?
* 2.校验redis中是否有值
* *
* 3.知识点补充:
* 指定参数名称进行传值,运行期绑定参数类型完成注解的拦截
* joinPoint必须位于参数的第一位.
*/
@Around("@annotation(cacheFind)")
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind){
Object result = null;
//key=业务名称::参数
String key = cacheFind.key();
String args = Arrays.toString(joinPoint.getArgs());
key = key + "::" + args;
//2.校验是否有值
if(jedis.exists(key)){
String json = jedis.get(key);
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class returnType = methodSignature.getReturnType();
result = ObjectMapperUtil.toObj(json,returnType);
System.out.println("AOP查询redis缓存");
}else{
//redis中没有数据,所以需要查询数据库,将数据保存到缓存中
try {
result = joinPoint.proceed();
String json = ObjectMapperUtil.toJSON(result);
//是否设定超时时间
if(cacheFind.seconds()>0){
jedis.setex(key, cacheFind.seconds(), json);
}else{
jedis.set(key,json);
}
System.out.println("AOP查询数据库");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
return result;
}
/**
* //1.获取key 注解 方法对象 类 方法名称 参数
* Class targetClass = joinPoint.getTarget().getClass();
* //2.获取方法对象
* String methodName = joinPoint.getSignature().getName();
* Object[] args = joinPoint.getArgs();
* Class[] classArgs = new Class[args.length];
* for(int i=0;i<args.length;i++){
* classArgs[i] = args[i].getClass();
* }
* try {
* //反射实例化对象
* Method method = targetClass.getMethod(methodName,classArgs);
* CacheFind cacheFind = method.getAnnotation(CacheFind.class);
* String key = cacheFind.key();
* System.out.println(key);
* } catch (NoSuchMethodException e) {
* e.printStackTrace();
* }
*/
//公式 aop = 切入点表达式 + 通知方法
//@Pointcut("bean(itemCatServiceImpl)")
//@Pointcut("within(com.jt.service.*)")
//@Pointcut("execution(* com.jt.service.*.*(..))") //.* 当前包的一级子目录
/* @Pointcut("execution(* com.jt.service..*.*(..))") //..* 当前包的所有的子目录
public void pointCut(){
}*/
//如何获取目标对象的相关参数?
//ProceedingJoinPoint is only supported for around advice
/* @Before("pointCut()")
public void before(JoinPoint joinPoint){ //连接点
Object target = joinPoint.getTarget();
Object[] args = joinPoint.getArgs();
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
System.out.println("目标对象:"+target);
System.out.println("方法参数:"+Arrays.toString(args));
System.out.println("类名称:"+className);
System.out.println("方法名称:"+methodName);
}*/
}