Lesson10 京淘项目中整合Redis数据库(通过查redis展现商品类目的tree)

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;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值