1.Redis入门案例
1.1 准备工作,切换回windows开发环境
linux系统的学习暂时告一段落了,现在要恢复到windows系统中操作京淘项目,
所以要切换回windows的京淘项目开发环境。
但Redis数据库我是部署在了Linux系统中,所以虚拟机还是得开着的。(192.168.126.129)
1)windows中jt-manage项目的tomcat服务器的端口号:8091
2).yml配置文件中mysql数据库的url:
url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
3)image.properties中图片的上传路径
image.dirPath=D:/CGB2005IV/imgdemo
4)host文件: 127.0.0.1 image.jt.com
127.0.0.1 manage.jt.com
5)修改windows中的nginx的配置
不用再配置那么多tomcat服务器,所以不用使用集群了。只用1台tomcat服务器。
1.2 导入redis的jar包
在jt父级项目的pom文件中添加redis的jar包依赖
注意:程序中调用API的对象名叫Jedis
<!--spring整合redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
1.3 练习redis的常用命令和API
新建个测试类,练习练习redis的常用API
注意:测试类名上面不要加@SpringBootTest注解,只有测试类中需要注入spring管理的对象时,才要加这个注解,否则不要乱加。
但方法上记得加@Test注解
String类型的数据练习
public class TestRedis {
/** test01
* 测试远程redis(linux中的redis服务器)是否可用
* host:192.168.126.129
* port:6379
* 思路:1.new一个Jedis对象
* 2.利用对象.方法的形式执行redis命令
*/
@Test
public void test01(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.set("redis1","测试redis数据库是否可用");
System.out.println(jedis.get("redis1"));
}
/** test02
* redis中String类型的API练习
* 需求:判断key "redis1" 是否存在,如果存在,则不赋值,
* 否则给这个key赋个值abc,并存入redis数据库。
* 原始写法,代码很多
*/
@Test
public void test02(){
Jedis jedis = new Jedis("192.168.126.129",6379);
if(jedis.exists("redis1")){
System.out.println("redis1 这个key已存在");
}else {
jedis.set("redis1","abc");
}
System.out.println(jedis.get("redis1"));
}
/** test03
* redis中String类型的API练习
* 需求:判断key是否存在,如果存在,则不赋值,否则入库。
* 利用setnx()优化写法,代码减少
* (set no exist) 如果key已存在,则不对这个key,以及它对应的value做任何改变。
* 如果key不存在,则把新的value赋值给这个key,并存入数据库
*/
@Test
public void test03(){
Jedis jedis = new Jedis("192.168.126.129",6379);
//清空redis数据库
jedis.flushAll();
//如果key存在,则不作任何操作
jedis.setnx("redis1","第一次赋的值!");
jedis.setnx("redis1","第二次赋的值!!");
System.out.println(jedis.get("redis1")); //第一次赋的值!
}
/** test04
* 测试添加超时时间的有效性
* 涉及到的知识点:原子性。各个语句要成功,就都成功,要是有一个失败,就都回滚。
* 业务:想aa中保存一个数据后,要求10s后失效
*/
@Test
public void test04() {
Jedis jedis = new Jedis("192.168.126.129",6379);
// 1.没考虑到 原子性的写法
// jedis.set("aa", "abc");
// int a =1/0; //到这就会报错,下面那句就不执行了。 但把aa存入数据库中会执行成功,不会回滚。所以这么写不行。
// jedis.expire("aa", 10);
//2.优化后,有了原子性的写法。通过setex()把赋值和设定超时时间写在同一个方法中。
try {
jedis.setex("aa", 10, "abc");//时间单位是秒
Thread.sleep(10);//让程序睡11秒,aa会被清空
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(jedis.get("aa")); //null
}
/** test05
* 需求:添加一个数据,要求只有数据存在时(有K-V),才会赋值,并且添加超时时间。
* 要是没有这么一个key值,不会为这个key赋任何值。
* 保证原子性操作。
* private static final String XX = "xx"; 要是已经有这个key了,就给这个key赋新值,入库。
* private static final String NX = "nx"; 没有这个想要的key(比如:aa)的时候,新建这个key,并赋值,入库。
* private static final String PX = "px"; 这个key存活的时间是 多少多少 毫秒
* private static final String EX = "ex"; 这个key存活的时间是 多少多少 秒
*
* 在redis分布式锁的问题时 会用到这种情况。
* 场景: A在上厕所(已经有了这个key,xx()),而且规定上厕所时间最长为60秒(ex(60))。如果超时了,这个厕所就会被清空(为null)
*/
@Test
public void test05(){
Jedis jedis = new Jedis("192.168.126.129",6379);
try {
jedis.set("aa","qwer");//这道题的前提是得先有一条key是aa的数据,所以我先创建一个
SetParams setParams = new SetParams();
setParams.xx().ex(5);
jedis.set("aa", "abc",setParams); //它发现前面有一个key为aa的数据,就给aa赋了个新值“abc”,并设定存活时间为5s
Thread.sleep(5100);
System.out.println(jedis.get("aa")); //睡了5.1秒后,再打印出来,会发现jedis已经被清空了,为null了。如果不睡,打印出来的就是“abc”。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Hash类型的数据练习:
用于保存对象和对象的属性名和属性值
eg:User对象{“id”:“1”}
Student对象{“name”,“Tom”}
hset 为对象添加数据
hget 获取对象的属性值
hexists 判断对象的属性是否存在
hdel 删除hash中的属性
hgetall 获取hash全部元素和值
hkyes 获取hash中的所有字段
hvals 获取hash的所有值
hmset 为hash的多个字段设定值
hmget 获取hash里面指定字段的值
List类型的数据练习:
说明:Redis中的List集合是双端循环列表,分别可以从左右两个方向插入数据.
List集合可以当做队列使用,也可以当做栈使用
队列的特点:先进先出
栈的特点:先进后出
/**
* 要求:将1,2,3,4从左向右 插入队列中。
* 然后在右侧一个一个地取值。
*/
@Test
public void testList(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.flushAll();
jedis.lpush("aa", "1","2","3","4");
System.out.println(jedis.rpop("aa")); //1
System.out.println(jedis.rpop("aa")); //2
System.out.println(jedis.rpop("aa")); //3
System.out.println(jedis.rpop("aa")); //4
System.out.println(jedis.rpop("aa")); //null
}
Redis事务命令练习:
/**
* 开启redis事务,给aa,bb赋值,并通过int a = 1/0;让程序出错。看其是否回滚。
*/
@Test
public void testTx(){
Jedis jedis = new Jedis("192.168.126.129",6379);
jedis.flushAll();
Transaction transaction = jedis.multi(); //开启事务
try {
transaction.set("aa","abc"); //让这个给aa赋值的指令等待准备执行
transaction.set("bb","defg"); //让这个给bb赋值的指定等待准备执行
int a = 1/0;
transaction.exec(); //开始执行上面两条赋值的指令
System.out.println(jedis.get("aa")); //能得到aa的值
}catch (Exception e){
transaction.discard(); //让上面两条赋值的语句进行回滚。
System.out.println("回滚了"+jedis.get("aa")); //如果执行了int a = 1/0; 就会回滚,aa对应的V就为null
}
}
2 SpringBoot整合Redis
一个项目中,什么样的功能适合用缓存?
查询的业务,
并且是操作比较频繁的查询业务,
并且还要内容不经常变化的。
像上面1.3中测试时,每次都需要我手动去new Jedis对象,很麻烦。
并且不仅manage系统中用会用到redis ,别的系统中可能也会用到redis,每次都手动new Jedis对象,非常麻烦。
在SpringBoot项目中怎么把Jedis交给Spring容器去管理呢?
方法:
写一个配置类,在返回Jedis对象的方法上写一个@Bean注解。这样Jedis对象就交给spring管理了,以后其他服务器中的方法就可以直接调用Jedis对象了。
2.1 配置类应该设置在哪里?
由于redis以后会被很多服务器使用,所以最好的方式将Redis的配置类保存到jt-common中.
2.1.1 将host和port写到一个properties文件中
由于在new Jedis对象的时候要传两个参数host和port,要是把它俩写死在方法中,就太死板了,可以把它俩写到一个properties文件中,取名为redis。
然后在程序中用@Value("${redis.host}")
和@Value("${redis.port}")
就能获取到。
2.1.2 编辑Jedis配置类—将redis对象(Jedis)交给Spring管理
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 {
@Value("${redis.host}") //从redis.properties文件中获取host
private String host;
@Value("${redis.port}")//从redis.properties文件中获取port
private Integer port;
/**
* 将jedis对象交给spring容器管理
*/
@Bean //默认条件下是单例对象
public Jedis jedis(){
//如果将IP和端口写死,不利于后期扩展。所以把IP和端口最好写在配置文件中。
return new Jedis(host,port);
}
}
2.2 对象与json字符串之间的相互转换
在项目中 如果用了redis数据库,由于redis数据库中的数据类型是key-value类型。与json字符串特别类似。
而项目中有很多类型的对象,如果想存到redis数据库中,就要先转化为json类型。
所以现在就需要研究各种类型的对象与json字符串之间怎么进行相互转换
2.2.1 普通类型的对象与json字符串之间的相互转换
写个测试类研究研究:
//普通对象与JSON字符串之间的互相转换
@Test
public void test01(){
//ObjectMapper是jackson包中提供的对象
ObjectMapper objectMapper = new ObjectMapper();
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(100L);
itemDesc.setItemDesc("ItemDesc对象转变为JSON字符串");
itemDesc.setCreated(new Date()).setUpdated(new Date());
try {
//1. 将普通对象 转换为 JSON字符串
//通过objectMapper对象去调用writeValueAsString方法,想转谁,就把谁当做方法参数。
String itemJson = objectMapper.writeValueAsString(itemDesc);
System.out.println(itemJson);
//json字符串的一种形式 {key1:value1,key2:value2,key3:value3....}
// {"created":1599718685339,"updated":1599718685339,"itemId":100,"itemDesc":"ItemDesc对象转变为JSON字符串"}
//2. 将JSON字符串 转换为 普通对象。(只能通过反射的方法)
//表明要转换成什么类型的对象(xxxx.class),然后实例化对象。在利用对象的get/set方法为属性赋值。
ItemDesc itemDesc1 = objectMapper.readValue(itemJson, ItemDesc.class);
System.out.println(itemDesc1);
//System.out.println(itemDesc1.toString()); //上面那句其实是这句的简写形式
//ItemDesc(itemId=100, itemDesc=ItemDesc对象转变为JSON字符串)
//@Data注解决定了只显示这两个属性的值,created和updated的值其实也有,只是没显示。
//不信的话 ,打印一下
System.out.println(itemDesc1.getCreated());
//Thu Sep 10 16:08:25 CST 2020
}catch (Exception e){
e.printStackTrace();
}
}
2.2.2 List集合与json字符串之间的相互转换
//List集合与json字符串之间的互相转换
@Test
public void test02(){
//ObjectMapper是jackson包中提供的对象
ObjectMapper objectMapper = new ObjectMapper();
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(100L);
itemDesc.setItemDesc("ItemDesc对象转变为JSON字符串");
itemDesc.setCreated(new Date()).setUpdated(new Date());
List<ItemDesc> list = new ArrayList<>();
list.add(itemDesc); //将itemDesc对象存入list集合中
list.add(itemDesc); //由于list集合可以存储重复的数据,为了偷懒,我就存了2个相同的itemDesc对象
try {
//1.将集合对象转化为JSON字符串
String listJson = objectMapper.writeValueAsString(list);
System.out.println(listJson);//json字符串有2种形式,这个就是其中一种,[{key1:value1},{key2:value2}]
//[{"created":1599721508979,"updated":1599721508979,"itemId":100,"itemDesc":"ItemDesc对象转变为JSON字符串"},{"created":1599721508979,"updated":1599721508979,"itemId":100,"itemDesc":"ItemDesc对象转变为JSON字符串"}]
//2.将JSON字符串转化为集合
List<ItemDesc> jsonList = objectMapper.readValue(listJson, list.getClass());
System.out.println(jsonList);
//[{created=1599721508979, updated=1599721508979, itemId=100, itemDesc=ItemDesc对象转变为JSON字符串}, {created=1599721508979, updated=1599721508979, itemId=100, itemDesc=ItemDesc对象转变为JSON字符串}]
}catch (Exception e){
e.printStackTrace();
}
}
2.2.3 通过封装一个工具API,简化对象与json相互转化过程的代码
上面的方法中,都写了try{}catch(){},使得代码整体很混乱。
所以可以先写一个工具API-----ObjectMapperUtil,简化代码的调用。
说明:在jt-common中添加工具API对象ObjectMapperUtil
package com.jt.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.StringUtils;
public class ObjectMapperUtil {
/**
* 把MAPPER对象定义为常量对象
* 优势1: 只需要new一份对象,节省空间
* 优势2: 可以实现 对象不允许在被调用时被随意修改!
*/
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 1.将任意类型的对象转化为JSON字符串
* 思考1:任意对象 应该用Object对象来接
* 思考2:返回值是JSON字符串,所以返回值类型是String
* 思考3:使用什么方式转化为JSON
*/
public static String toJSON(Object object){
try {
if(object==null){
throw new RuntimeException("传递的对象为空,请检查");
}
return MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
e.printStackTrace();
//如果出了异常,应该将这种检查异常转化为运行时异常。
throw new RuntimeException("传递的对象不能转化为JSON字符串/检查该对象是否有set/get方法");
}
}
/**
* 2.将JSON字符串转化为指定类型的对象(可以指定任意类型)
*
*/
public static <T> T toObject(String json,Class<T> target){
if(StringUtils.isEmpty(json) || target ==null ){
throw new RuntimeException("传递的JSON字符串或者对象的类型 都不能为空");
}
try {
return MAPPER.readValue(json,target);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException("这个JSON字符串不能转化为指定类型的对象");
}
}
}
2.2.4 测试工具API是否可用
在ObjectMapperTest中添加测试方法
/**
* 如果像test02中那么写,每次都要写大量的try-catch,会很乱。
* 利用我自己写的工具API:ObjectMapperUtil 就简化了test02中的代码
*/
@Test
public void test03(){
ItemDesc itemDesc = new ItemDesc();
itemDesc.setItemId(100L);
itemDesc.setItemDesc("ItemDesc对象转变为JSON字符串");
itemDesc.setCreated(new Date()).setUpdated(new Date());
//将任意类型的对象转化给JSON字符串
String json = ObjectMapperUtil.toJSON(itemDesc);
System.out.println(json);
//{"created":1599724903160,"updated":1599724903160,"itemId":100,"itemDesc":"ItemDesc对象转变为JSON字符串"}
//将json字符串转化为指定类型的对象(可以指定为任意类型,这里指定为ItemDesc类型)
ItemDesc itemDesc1 = ObjectMapperUtil.toObject(json, ItemDesc.class);
System.out.println(itemDesc1);
//ItemDesc(itemId=100, itemDesc=ItemDesc对象转变为JSON字符串)
}
2.3 实现商品分类的缓存
2.3.1 修改ItemCatController
/**
* 分析业务:实现商品分类的树结构的展现
* 1.url地址:http://localhost/item/cat/list
* 2.参数: parentId 查询商品分类菜单
* 3.返回值结果: List<EasyUITree>
*/
@RequestMapping("/list")
public List<EasyUITree> findItemCatList(Long id){
//当初始时 树形结构 还没加载呢,不会传递ID,所以此时ID为null,所以此时parentId为0
Long parentId = (id==null)?0:id;
//return itemCatService.findItemCatList(parentId);
//用查redis缓存的方式去实现分类树结构的展现
return itemCatService.findItemCatch(parentId);
}
2.3.2 修改ItemCatServiceImpl
/**
* 用查redis缓存的方法 实现分类树的展现
* 步骤:
* 1.先查询redis缓存:
* true 直接从缓存中拿数据
* false 去查询数据库
*
* key有什么特点:
* 1.key应该动态变化
* 2.通过key 一眼就能看出来 这个key后面接的value代表的是什么。
* 所以 把parentId当做key最合适
* 由于key的名字可以自定义,所以我就命名成类似于(ITEM_CAT_PARENTID_0,ITEM_CAT_PARENTID_1,ITEM_CAT_PARENTID_2....)
* 的形式,但这样就写死了。
* 要让客户传什么parentId,就把这个parentId拼接在ITEM_CAT_PARENTID后面。
* 前人规定的写法 "ITEM_CAT_PARENTID::"+parentId :: 的意思就是 后面接的parentId属于 前面ITEM_CAT_PARENTID中的一个
* key = ITEM_CAT_PARENTID::parentId
*
* 把List转成Key
*/
@Override
public List<EasyUITree> findItemCatch(Long parentId) {
/**
* 商品分类树信息查询的宗旨就是 根据 parentId去查询子类的id,text和state信息。
* 而redis数据库中数据存储的形式是key-value的形式。
* 要想把商品分类信息保存到redis数据库中,就要以K-V的形式。
* K就是parentId,V就是商品的id,text和state
*
*/
//1.定义List<EasyUITree> treeList ,里面什么都么有。
List<EasyUITree> treeList = new ArrayList<>(); //这不能写null,为了后面treeList.getClass()能获取到treeList的类型
//所以redis数据库中key就是下面的样子
String key = "ITEM_CAT_PARENTID::"+parentId;
//2.有了Key,就可以从redis数据库中查询数据(对应的商品分类信息value)了
String itemCatValue = jedis.get(key);
//打个桩================================
System.out.println("itemCatValue="+itemCatValue);
//例如 查询“手机”类目下
//itemCatValue=[{"id":559,"text":"手机通讯","state":"closed"},{"id":562,"text":"运营商","state":"closed"},{"id":567,"text":"手机配件","state":"closed"}]
//3.由于不能保证json字符串中一定有数据,如果根据parentId没查到对应的value,那么String json就是null,所以要校验json中是否有值。
if(StringUtils.isEmpty(itemCatValue)){
//4.如果itemCatValue为空,说明在缓存中没查到这个parentId对应的商品分类信息。那就去数据库中查查。
//调用第41行的方法
treeList = findItemCatList(parentId);
//由于treeList定义在了if代码块里面,而它将来就是要被return 回去的。在代码块里显然是return不走的。
//所以我在外面定义了一个List<EasyUITree> treeList 先让它为一个空集合。
//4.为了从第二次开始,查商品分类树时,不再去查数据库了,就要把商品分类数据treeList添加到redis数据库中
//将treeList这个List集合对象转化为json字符串的形式
itemCatValue = ObjectMapperUtil.toJSON(treeList);
jedis.set(key,itemCatValue);//这里不能直接写treeList。因为redis数据库中的数据中的K-V都是string类型,而treeList是List<EasyUITree>类型,是个list集合。
System.out.println("这是第一次从数据库查询出来的~~");//打个桩===============================
}else {
//如果进入到这个代码块中,说明redis缓存中已经有了这个商品分类信息,所以要将这个json数据转化为List<EasyUITree>对象
treeList = ObjectMapperUtil.toObject(itemCatValue,treeList.getClass());
System.out.println("这个结果是从redis数据库查出来的~~~");
}
return treeList;
}