CGB2005-京淘12(Redis入门案例,分布式锁,SpringBoot整合Redis,redis配置类,缓存场景,ObjectMapper 泛型T,商品分类的redis缓存)

注意事项

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.

作业

  1. 完成Redis案例测试
  2. 将Redis命令了解 官网命令 Set/zSet
  3. AOP优化缓存.
    1.利用自定义的注解 @CacheFind 标识缓存业务.(博客)
    2.了解通知的类型
    3.了解切入点表达式
    4.复习AOP的工作原理

猜你喜欢

转载自blog.csdn.net/aa35434/article/details/108520054