注意事项
1.redis入门案例:引入jar包 编写测试类
2.秒杀业务逻辑—分布式锁机制
3.SpringBoot整合Redis
编译配置类优化jedis对象的创建
缓存适用场景分析
对象与JSON互转–ObjectMapper---->封装为工具api
实现商品分类缓存(树状–选择类目)
1. Redis入门案例
在测试类中创建包,类。
注意:测试类所在的包也应该在主启动类的包或它的子包下。
1.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.1.2 入门测试案例
manage测试类中
package com.jt.test;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.params.SetParams;
//@SpringBootTest 如果需要在测试类中引入spring容器机制才使用该注解,如属性注入时.
public class TestRedis {
/**
* 1.测试远程redis服务器是否可用
* 思路:
* 1.实例化jedis工具API链接对象(host:port)
* 2.利用对象执行redis命令 方法就是命令
* 报错调试:
* 1.检查Redis.conf的配置文件是否按照要求修改 ip/保护/后台
* 2.redis启动方式 redis-server redis.conf
* 3.关闭防火墙 systemctl stop firewalld.service
* */
@Test
public void test01(){
String host="192.168.126.129";//redis所在的ip地址
int port =6379;//redis的端口
Jedis jedis =new Jedis(host,port);//注意这里导的是redis.clients包
jedis.set("cgb2006","好好学习");
System.out.println(jedis.get("cgb2006"));
}
/**
* 2.需求:
* 1.向redis中插入数据 k-v
* 2.为key设定超时时间 60秒后失效
* 3.线程sleep 3秒
* 4.获取key的剩余存活时间
*
* 问题描述:下面的代码存在bug,数据一定会被删除吗???若中间出现了异常,下面的数据就不会执行。
* 问题说明:如果使用redis并且需要添加超时时间 一般需要满足原子性要求。
* 原子行: 要么同时成功,要么同时失败.注意:必须同时完成。
*/
@Test
public void test02() throws InterruptedException {
/* 优化前的写法:
Jedis jedis=new Jedis("192.168.126.129",6379);
jedis.set("宝可梦", "小火龙");
int a=1/0;//抛出异常。
jedis.expire("宝可梦",60);//设置key的有效时间
Thread.sleep(3000);//1秒=1000毫秒
System.out.println(jedis.ttl("宝可梦"));//检查key的剩余时间*/
/** 优化后的写法:
* 如果想对数据添加超时时间,redis又提供了优化后的api方法。
* */
Jedis jedis=new Jedis("192.168.126.129",6379);
jedis.setex("宝可梦",60,"小火龙");
System.out.println(jedis.get("宝可梦"));
}
/**
* 3.需求:
* 如果发现key已经存在时不修改数据,如果key不存在时才会修改数据。
* 问题:这样看着代码if els层级太多,redis提供了优化后的写法,代替if-else。
*/
@Test
public void test03(){
Jedis jedis=new Jedis("192.168.126.129",6379);
/* if(jedis.exists("redis")){
System.out.println("key已存在,不做修改");
}else{
jedis.set("redis", "aaaa");
}*/
//解释:如果redisa存在了则不修改,取原来的值,如果redisa不存在取值"测试nx的方法"
jedis.setnx("redisa", "测试nx的方法");
System.out.println(jedis.get("redisa"));
}
/**
* 4.需求:
* 1.要求用户赋值时,如果数据存在则不赋值。setnx
* 2.要求在赋值操作时,必须设定超时的时间 并且要求满足原子性 settex
* 问题:这2个方法不能同时用,那么同时满足这2种需求需要学习新方法 set的重载方法
*/
@Test
public void test04(){
Jedis jedis=new Jedis("192.168.126.129",6379);
SetParams setParams=new SetParams();
//nx:key存在不赋值 ex:秒 xx:有key的时候才赋值 px:毫秒
setParams.nx().ex(10);//加锁操作
jedis.set("bbbb", "实现业务操作",setParams );
System.out.println(jedis.get("bbbb"));
jedis.del("bbbb"); //解锁操作
}
@Test
public void testList05() throws InterruptedException{
Jedis jedis=new Jedis("192.168.126.129",6379);
jedis.lpush("list", "1","2","3");
System.out.println(jedis.rpop("list"));
}
@Test
public void testTx06() throws InterruptedException{
Jedis jedis=new Jedis("192.168.126.129",6379);
//1.开启事务
Transaction transaction=jedis.multi();
try {
transaction.set("aa","aa");
//2.提交事务
transaction.exec();
} catch (Exception e) {
e.printStackTrace();
//3/回滚事务
transaction.discard();
}
}
}
2. 秒杀业务逻辑—分布式锁机制
原价:6988 —>现价1块
问题描述:1部手机 20各人显示抢购成功 并且支付了1块钱…
问题说明:
1.tomcat服务器有多台
2.数据库数据只有1份(主从库数据是只有一份的)
3.必然会出现高并发的现象.(多个人抢购)
如何实现抢购???
2.1 同步锁操作
2.1.1 超卖的原因
分析:a买手机,去查询数据库,数据库存量减去1,b这时也访问但是数据库还没有减这时数据库中还有一台手机,这是还回去访问数据库减去1.
说明:tomact是多线程操作,多线程抢占同一资源必然导致线程安全问题。
2.1.2 同步锁的问题
说明:同步锁只能解决tomcat内部的问题,不能解决多个tomcat并发问题。
分析:虽然在tomact内部加了同步锁,但是tomact服务器有多个,还是会出现线程安全问题。
2.2 分布式锁机制
思路:
1.锁应该使用第三方操作 ,锁应该公用.
2.原则:如果锁被人正在使用时,其他的用户不能操作.
3.策略: 用户向redis中保存一个key,如果redis中有key表示有人正在使用这把锁 ,其他用户不允许操作.如果redis中没有key ,则表示我可以使用这把锁.
4.风险: 如何解决死锁问题. 设定超时时间.
面试回答什么是分布式锁???
分布式锁一般在第三方人人都可以用的,一般分布式锁用Redis实现,向Redis中添加数据,key就是锁,value就是密码。tomact服务器先访问redis,如果key存在则不能执行,key不存在才能执行,并且把key-value存进redis中。加锁可能会出现一些死锁的情况出现,所以在加锁的时候添加超时时间,但是解锁的代码一般放在finally里,finally里的代码谁都可以使用,所以避免锁被别人提前释放需要加上一些密码校验,只有密码一致才能把锁去掉。 最终实现我家的锁只能我解,即使不解过一段时间也会释放掉。
问题:那会不会出现同时加锁的情况??? 不会,因为redis是单线程操作。
3. SpringBoot整合Redis
3.1 配置类的位置说明
说明:
1).在入门案例中每次使用redis都需要new一个Jedis对象,比较麻烦。所以最好是把常见对象的权利交给spring容器去管理,哪个地方需要使用就用@Autwried注解注入即可。
2).由于redis之后会被其它的服务器使用,所以最好的方式将Redis的配置类保存到common中。
3.2 编辑Pro文件类
说明:因为这个配置是公共的,所以放在conmon项目下的配置目录中.
3.3 编辑配置类JedisConfig(common中)
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 //标识我是一个配置类
@PropertySource("classpath:/properties/redis.properties") //写活的形式,导入配置文件
public class JedisConfig {
//这里面的key不允许重复,如果properties和yml都有相同的key则以yml为准
@Value("${redis.host}") //这是spel表达式(springel) 而el是jsp的表达式
private String host;
@Value("${redis.port}")
private Integer port;
/**
* 将jedis对象交给spring容器管理
*/
@Bean
public Jedis jedis(){
//由于将代码写死不利于扩展,所以将固定的配置添加到配置文件中
return new Jedis(host,port);
}
}
3.4 缓存适用场景分析
3.4.1 什么数据可以放缓存
说明:
1.不需要实时更新但是又极其消耗数据库的数据。
2.需要实时更新,但是更新频率不高的数据。
3.在某个时刻访问量极大而且更新也很频繁的数据。但是这种数据使用的缓存不能和普通缓存一样,这种缓存必须保证不丢失,否则会有大问题。
总结:缓存适合数据变化不大,但经常查询数据库的业务。
3.4.2 分析jt-manage那些业务适合缓存
1.分页查询:数据新增后数据库记录会放生变化,在分页页面上整体顺序会发生改变,像这种变化比较大的数据不适合做缓存。
2.叶子类目适合做缓存:每次刷新页面不管页面是否发生变化都会在后台进行一次查询。
3.选择类目适合缓存:只要点击父级目录就会访问数据库进行查询。
3.5 对象与JSON互转–ObjectMapper
3.5.1 原因分析
问题:为什么要进行转化???
原因:现在树形目录查询的数据是储存在List< EasyUITree>这个集合对象中,而redis中要求储存的类型大部分是String类型 ,所以现在查询的数据无法直接存进redis缓存中。
解决:把对象通过Api中的方法转化为json字符串存进redis中。因为Redis本质上是String字符串,取值的时候在通过API转化为对象取出来即可。
API:
JSON原生提供:ObjectMapper
阿里提供:Fastjson
思考:
1).直接使用List< EasyUITree>.toString()
转化为字符串存入redis中不行吗???
答:不行,虽然可以转化为字符串,但是没有办法取值的时候把字符串还原为对象。
2).用@ResponseBody这个注解把对象转化为json字符串行么???
答: 不行,此注解相当于告诉spring MVC方法的返回值转化为JSON, 而现在是把数据在业务层方法中使用,所以这种方法不适合. 所以只能学习一套API实现对象与JSON数据的转化.
3.5.2 ObjectMapper入门案例
manage测试类中
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 {
//简单对象与JSON互转:
@Test
public void test01(){
//定义一个工具API对象
ObjectMapper objectMapper = new ObjectMapper();
//创建商品表对象测试
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(100L).setItemDesc("json测试")
.setCreated(new Date()).setUpdated(new Date());
try {
/**
* 1.将对象转化为json,因为赋的值可能不规范,不是传什么值都能转化为json有风险,
* 所以需要处理异常,捕获或者抛出.
*/
String result = objectMapper.writeValueAsString(itemDesc);
System.out.println(result);
/**
* 2.将json数据转化为对象,字符串和对象其实不能直接转化,只能通过反射机制..
*反射: 给定xxx.class类型之后实例化对象.利用对象的get/set方法为属性赋值.
*第一个参数是需要转化的数据 第二个参数是转化的对象类型.
*/
ItemDesc itemDesc2 = objectMapper.readValue(result,ItemDesc.class);
System.out.println(itemDesc2.getCreated());//输出父级的属性创建时间
System.out.println(itemDesc2.getItemDesc());//输出自己的ItemDesc属性
/*输出对象,但是结果只有2个,我们实际上赋值了有4个数据为什么会这样呢???
因为这个方法我们用的是@Data注解生成的,这个注解有个特点,只显示
自己的属性不显示父级的属性,实际上还是有的只是不显示.*/
System.out.println(itemDesc2.toString());
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
//集合对象与JSON互转:
@Test
public void test02(){
ObjectMapper objectMapper = new ObjectMapper();
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(100L).setItemDesc("json测试1")
.setCreated(new Date()).setUpdated(new Date());
ItemDesc itemDesc2 = new ItemDesc();
itemDesc2.setItemId(100L).setItemDesc("json测试2")
.setCreated(new Date()).setUpdated(new Date());
List<ItemDesc> list = new ArrayList<>();
list.add(itemDesc);
list.add(itemDesc2);
try {
//1.将对象转化为JSON (用的是同一个方法)
String json = objectMapper.writeValueAsString(list);
System.out.println(json);
//2.将json转化为对象 转化的json串 转化的类型:list集合以对象的形式获取类型.
List<ItemDesc> list2 = objectMapper.readValue(json, list.getClass());
System.out.println(list2);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
3.6 封装ObjectMapperUtil(commom中)
3.6.1 业务说明
说明:实际上在业务中需要使用这个Api进行转化时,异常往往需要自己处理(try–catch)而不是抛出。但是在代码中try–catch过多会导致结构混乱,所以编写工具Api进行简化。
步骤:
方法1: 将任意的对象转化为JSON.
方法2: 将任意的JSON串转化为对象.
要求完成异常的处理.
3.6.2 定义工具API
package com.jt.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.StringUtils;
public class ObjectMapperUtil {
/**
* 说明:每次使用都要要创建一个对象太麻烦,定义常量对象只需定义一次即可。
* 优势1: 对象独一份节省空间
* 优势2: 对象不允许别人随意篡改
*/
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 1.将任意对象转化为JSON
* 思考1: 任意对象对象应该使用Object对象来接
* 思考2: 返回值是JSON串 所以应该是String
* 思考3: 使用什么方式转化JSON FASTJSON(阿里的)/objectMapper(原生自带的)
*/
public static String toJSON(Object object){//定义静态的方便调用
try {
if(object == null){ // 判断用户传的数据是否为空
throw new RuntimeException("传递的参数object为null,请认真检查");
}
return MAPPER.writeValueAsString(object);//没有异常直接转化
} catch (JsonProcessingException e) {
e.printStackTrace();
//转化过程中出了异常,该怎么处理?应该将检查异常,转化为运行时异常.
throw new RuntimeException("传递的对象不支持json转化/检查是否有get/set方法");
}
}
/**
* 2.将任意的JSON串转化为对象
* 需求:用户传递什么样的类型,我返回什么样的对象
* <T> T 自定义了一个泛型对象,前后一致保证传的是什么类型定义的就是什么类型,返回值就是定义的泛型,
* 这种方式一般适用于工具API和源码的编译上。
*
*/
public static <T> T toObject(String json,Class<T> target){
//字符串操作的工具API
if(StringUtils.isEmpty(json) || target == null){
throw new RuntimeException("传递的参数不能为null");
}
try {
return MAPPER.readValue(json,target);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException("json转化异常");
}
}
}
3.7 实现商品分类的缓存(选择类目)
3.7.1 实现步骤
1.定义Redis中的key, key必须唯一不能重复. 存 取 应该保持一致。
parendid是唯一的可以作为key,为了见名知意在parentid前拼接关键字作为前缀,一般前缀和字符之间通过::
进行连接。
最终效果为:key = “ITEM_CAT_PARENTID::70”
2.根据key 去redis中进行查询,有数据或者没有数据
3.没有数据 则查询数据库获取记录,之后将数据保存到redis中方便后续使用.
4.有数据表示用户不是第一次查询 可以将缓存数据直接返回即可.
3.7.2 编辑ItemCatController
/**
* 业务需求: 实现商品分类的展现,户通过ajax请求,动态获取树形结构的数据.
* url地址: http://localhost:8091/item/cat/list
* 参数: parentId = 0 查询一级商品分类菜单.
* 返回值结果: List<EasyUITree> (因为要求的树状参数格式最外层是个[])
* 注意事项:
* 1.树形结构初始化时不会传递任何信息.只有展开子节点时传递Id,如果没有传递id,及初始化展现的是一级商品的id,展现的父级id就是0.
* 2.页面传递什么样的数据,后端必须接收什么样的数据
*/
@RequestMapping("/list")
public List<EasyUITree> findItemCatList(Long id){
//这个地方先测试查询一级菜单,所以先把id写死。
//用三木运算符判断。没有传id或传了id的情况。
Long parentId = (id==null?0L:id);//初始化没有传id,根据父级id=0就可以查询出一级菜单信息。
//return itemCatService.findItemCatList(parentId);
return itemCatService.findItemCache(parentId);
}
3.7.3 编辑ItemCatService
/**
* 改为从缓存中查询商品分类(树状),原先的方法还用的到。
* @param parentId
* @return
*/
@Autowired(required = false)//保证后续操作正常执行 可以懒加载(使用的时候在创建对象)
private Jedis jedis; //导包:redis.clients.jedis.Jedis;
@Override
public List<EasyUITree> findItemCache(Long parentId) {
//0.定义公共的返回值对象
List<EasyUITree> treeList = new ArrayList<>();
//1.定义key
String key = "ITEM_CAT_PARENTID::"+parentId;
Long startTime = System.currentTimeMillis();//记录开始时间
//2.检索redis服务器,是否含有key
if(jedis.exists(key)){
//3.数据存在 直接获取缓存数据,之后转化为对象
String json = jedis.get(key);//根据key获取value
long endTime = System.currentTimeMillis();//结束时间
treeList = ObjectMapperUtil.toObject(json,treeList.getClass());//json转对象
System.out.println("从redis中获取数据,耗时:"+(endTime-startTime)+"毫秒");
}else{
//4. 数据不存在 应该先查询数据库,之后将数据保存到缓存中.
treeList = findItemCatList(parentId);//直接调用上面从数据库查询的方法(原先的方法)
long endTime = System.currentTimeMillis();//结束时间
//4.1 将数据保存到缓存中
String json = ObjectMapperUtil.toJSON(treeList);//对象转json
jedis.setex(key, 7*24*60*60, json);//存入缓存并设置超时时间
System.out.println("查询数据库,耗时:"+(endTime-startTime)+"毫秒");
}
return treeList;
}
3.7.4 Redis速度测试
测试:先清空先redis缓存:flushall
问题1:查询缓存比查询数据库一般快20倍左右,为什么第一次查询差别那么大呢???
答:第一次需要先连接数据库建立通信,通信耗时间。第一次通信完之后会建立一种长连接的关系(短时间链接不会关闭),所以再次查询不用在建立连接节约时间。
问题2:
但是以上写缓存的方式不好,破坏了原始的代码结构,写业务代码加了很多缓存的代码,不好维护等…耦合性比较高,解决—用AOP.
作业
- 完成Redis案例测试
- 将Redis命令了解 官网命令 Set/zSet
- AOP优化缓存.
1.利用自定义的注解 @CacheFind 标识缓存业务.(博客)
2.了解通知的类型
3.了解切入点表达式
4.复习AOP的工作原理