01.Redis中间件实战1

142 阅读23分钟

Redis中间件实战1

Redis各种数据结构实战

字符串

需求:将用户信息存储至缓存中,实现每次前端请求获取用户个人详情时直接从缓存中获取。来演示字符串的写入与读取。 技术方案:为了实现这个需求,首先需要建立用户对象实体,里面包含用户个人的各种信息,包括ID、年龄、姓名、用户名及住址等, 然后采用RedisTemplate操作组件将这一用户对象序列化为字符串信息并写入缓存中,最后从缓存中读取即可。

public class Person implements Serializable {

    private int id;

    private int age;

    private String name;

    private String address;

    public Person() {

    }

    public Person(int id, int age, String name, String address) {
        this.id = id;
        this.age = age;
        this.name = name;
        this.address = address;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

创建controller

@RestController
@RequestMapping(value = "/string")
public class StringController {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper mapper;

    private static Logger logger = LoggerFactory.getLogger(StringController.class);

    @GetMapping(value = "/one")
    public void one() throws Exception{
        Person p1 = new Person(95001,23,"孙悟空","花果山水帘洞");
        // 定义key与即将存入缓存中的value
        final String key = "redis:string:1";
        // JSON序列化
        String value = mapper.writeValueAsString(p1);
        // 写入缓存中
        logger.info("存入缓存中的用户实体对象信息为:{}",p1);

        //使用set命令写入缓存中
        redisTemplate.opsForValue().set(key,value);

        // 从缓存中获取用户的信息
        final Object object = redisTemplate.opsForValue().get(key);

        if (object != null) {
            Person p = mapper.readValue(object.toString(),Person.class);
            logger.info("从缓存中读取信息:{}",p);
        }
    }
    
    @GetMapping(value = "/get")
    public Person one(String key) throws Exception{
         // 从缓存中获取用户的信息
         final Object object = redisTemplate.opsForValue().get(key);
         if (object != null) {
             Person p = mapper.readValue(object.toString(),Person.class);
             logger.info("从缓存中读取信息:{}",p);
             return p;
         }
         return null;
    }
}

测试 http://localhost:9090/swagger-ui.html

列表

Redis的列表类型跟java的List类型很类似,用于存储一系列具有相同类型的数据。其底层对于数据的存储和读取 可以理解为一个数据队列,往List中添加数据的时候,即相当于往队列中的某个位置插入数据;而从List中 获取数据相当于从队列中某个位置获取数据。

需求: 将一组已经排好序的用户对象列表存储在缓存中,按照排名的先后顺序获取出来并输出到控制台上。 技术方案: 首先需要定义一个已经排好序的用户对象的列表,然后将其存储到Redis的List中,最后按照排名的先后顺序 将每个用户实体获取出来。 代码实现: 创建controller

@RestController
@RequestMapping(value = "/list")
public class ListController {
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ObjectMapper mapper;
    private static Logger logger = LoggerFactory.getLogger(ListController.class);
    @GetMapping(value = "/one")
    public void one() {
        List<Person> pList = new ArrayList<>();

        pList.add(new Person(1,20,"孙悟空","花果山水帘洞"));
        pList.add(new Person(1,30,"猪八戒","高老庄"));
        pList.add(new Person(1,40,"沙悟净","流沙河"));
        pList.add(new Person(1,50,"唐三藏","东土大唐"));

        logger.info("构造已经排好序的用户对象列表对象:{}",pList);

        // 将列表数据存储到Redis中的List中
        final String key = "redis:list:1";
        ListOperations listOperations = redisTemplate.opsForList();

        for (Person p:pList) {
            // 往列表中添加数据-从队尾添加
            listOperations.leftPush(key,p);
        }

        // 获取Redis列表中的数据-从队头中遍历获取,直到没有元素为止。
        logger.info("获取Redis中list的数据-从队头中获取");
        Object res = listOperations.rightPop(key);
        Person tmp = null;
        while (res != null) {
            tmp = (Person)res;
            logger.info("当前数据:{}",tmp.getName());
            res = listOperations.rightPop(key);
        }
    }
}

日志

2024-02-05 15:04:20.782  INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController      : 构造已经排好序的用户对象列表对象:[com.airycode.redis.bean.Person@4900d96f, com.airycode.redis.bean.Person@2fe9acc6, com.airycode.redis.bean.Person@512f8c42, com.airycode.redis.bean.Person@32385a70]
2024-02-05 15:04:20.823  INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController      : 获取Redis中list的数据-从队头中获取
2024-02-05 15:04:20.850  INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController      : 当前数据:孙悟空
2024-02-05 15:04:20.853  INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController      : 当前数据:猪八戒
2024-02-05 15:04:20.856  INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController      : 当前数据:沙悟净
2024-02-05 15:04:20.859  INFO 13724 --- [nio-9090-exec-1] c.a.redis.controller.ListController      : 当前数据:唐三藏

总结: 在实际的应用场景中,Redis的列表类型特别适用于排名,排行榜,近期访问数据列表等业务场景 是一种很实用的存储类型

集合

Redis的集合类型跟高等数学中学习的集合类似,用于存储具有相同的类型或特性的不重复的数据 ,即Redis中的集合Set存储的数据是唯一的,其底层的数据结构是哈希表,所以添加,删除,查找 的时间复杂度均为O(1)

需求: 给定一组用户姓名列表,要求剔除具有相同姓名的人员并组成新的集合,存放至缓存中并用于前端的访问

解决方案: 首先构造一组用户列表,然后遍历访问,将姓名直接塞入Redis的Set集合中,集合底层会自动剔除重复的元素

核心代码:

@RestController
@RequestMapping(value = "/set")
public class SetController {


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper mapper;

    private static Logger logger = LoggerFactory.getLogger(SetController.class);
    
    @GetMapping("/one")
    public void one() throws Exception{

        // 构造用户列表
        List<Person> list = new ArrayList<>();

        list.add(new Person(1,20,"孙悟空","花果山水帘洞"));
        list.add(new Person(1,30,"猪八戒","高老庄"));
        list.add(new Person(1,40,"沙悟净","流沙河"));
        list.add(new Person(1,50,"唐三藏","东土大唐"));

        // 待处理的用户列表
        logger.info("待处理的用户列表{}",mapper.writeValueAsString(list));

        // 遍历访问,剔除相同的姓名的用户并存入集合中,最终存入缓存中
        final String key = "redis:set:1";

        SetOperations setOperations = redisTemplate.opsForSet();

        for (Person p:list) {
            setOperations.add(key,p);
        }
        // 从缓存中获取用户对象集合
        Object pop = setOperations.pop(key);
        while (pop != null) {
            logger.info("从缓存中获取的用户集合-当前用户{}",mapper.writeValueAsString(pop));
            pop = setOperations.pop(key);
        }
    }
}

日志:

待处理的用户列表[{"id":1,"age":20,"name":"孙悟空","address":"花果山水帘洞"},{"id":1,"age":30,"name":"猪八戒","address":"高老庄"},{"id":1,"age":40,"name":"沙悟净","address":"流沙河"},{"id":1,"age":50,"name":"唐三藏","address":"东土大唐"}]
2024-02-05 15:25:45.689  INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController       : 从缓存中获取的用户集合-当前用户{"id":1,"age":50,"name":"唐三藏","address":"东土大唐"}
2024-02-05 15:25:45.693  INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController       : 从缓存中获取的用户集合-当前用户{"id":1,"age":30,"name":"猪八戒","address":"高老庄"}
2024-02-05 15:25:45.696  INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController       : 从缓存中获取的用户集合-当前用户{"id":1,"age":20,"name":"孙悟空","address":"花果山水帘洞"}
2024-02-05 15:25:45.699  INFO 12484 --- [nio-9090-exec-1] c.a.redis.controller.SetController       : 从缓存中获取的用户集合-当前用户{"id":1,"age":40,"name":"沙悟净","address":"流沙河"}

总结: Redis的集合类型确实可以保证存储的数据是唯一、不重复的。在实际 的互联网应用中,Redis的Set类型常用于解决重复提交、剔除重复ID等业务场景

有序集合

Redis的有序集合SortedSet跟集合Set具有某些相同的特性,即存储 的数据是不重复、无序、唯一的;而这两者不同之处在于SortedSet可以 通过底层的Score(分数、权重)值对数据进行排序,实际存储的集合数据既不重复又有序 可以说其包含了列表List、集合Set的特性。

需求: 找出一个星期内手机话费单次充值金额前6名的用户列表,要求按照充值的金额从大到小排序,并存入缓存中

技术方案: 首先构造一组对象列表,其对象信息包括用户手机号、充值话费、然后遍历对象,将其加入缓存SortedSet中 最终将其获取出来。

核心代码:

@RestController
@RequestMapping(value = "/sortedset")
public class SortedSetController {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper mapper;

    private static Logger logger = LoggerFactory.getLogger(SortedSetController.class);

    @GetMapping("/one")
    public void one() throws Exception{

        // 构造用户列表
        List<PhoneUser> list = new ArrayList<>();

        list.add(new PhoneUser("111",200));
        list.add(new PhoneUser("222",100));
        list.add(new PhoneUser("333",50));
        list.add(new PhoneUser("444",80));

        // 待处理的用户列表
        logger.info("构造一组无序的用户手机充值对象列表:{}",mapper.writeValueAsString(list));
        // 遍历访问,剔除相同的姓名的用户并存入集合中,最终存入缓存中
        final String key = "redis:sortedset:1";
        final ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        for (PhoneUser p:list) {
            // 将元素添加进有序集合SortedSet中
            zSetOperations.add(key,p,p.getMoney());
        }
        // 前端获取访问充值排名靠前的用户列表
        long size = zSetOperations.size(key);
        // 从小到大排序
        Set<PhoneUser> resSet = zSetOperations.range(key, 0, size);
        // 从大到小排序
//        Set resSet = zSetOperations.reverseRange(key, 0, size);
        // 遍历获取有序集合中的元素
        for (PhoneUser phoneUser:resSet) {
            logger.info("从缓存中读取手机充值记录排序列表,当前记录:{}",mapper.writeValueAsString(phoneUser));
        }
    }
}

日志:

2024-02-05 15:49:29.883  INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController     : 构造一组无序的用户手机充值对象列表:[{"phoneNum":"111","money":200},{"phoneNum":"222","money":100},{"phoneNum":"333","money":50},{"phoneNum":"444","money":80}]
2024-02-05 15:49:29.944  INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController     : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"333","money":50}
2024-02-05 15:49:29.944  INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController     : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"444","money":80}
2024-02-05 15:49:29.945  INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController     : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"222","money":100}
2024-02-05 15:49:29.945  INFO 692 --- [nio-9090-exec-8] c.a.r.controller.SortedSetController     : 从缓存中读取手机充值记录排序列表,当前记录:{"phoneNum":"111","money":200}

总结: Redis的有序集合类型SortedSet确实可以实现数据元素的有序排列 默认情况下,SortedSet的排序类型是根据得分Score参数的取值 从小到大排序,如果需要倒序排列,则可以调用reverseRange方法即可

在实际生产环境中,Redis的有序集合SortedSet常用于充值排行榜、积分排行榜、成绩排名等应用场景

哈希存储

Redis的哈希存储跟JAVA的HashMap类似,其底层数据结构是由key-value组成的映射表 ,而其value又是由Field-Value,特别适用于具有映射关系的数据对象的存储。

需求:Redis的哈希数据结构的使用

核心代码:

@RestController
@RequestMapping(value = "/hash")
public class HashController {


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper mapper;

    private static Logger logger = LoggerFactory.getLogger(HashController.class);

    @GetMapping("/one")
    public void one() throws Exception{

        // 构造学生对象列表和水果对象列表
        List<Student> students = new ArrayList<>();
        List<Fruit> fruits = new ArrayList<>();
        // 往学生集合中添加学生对象
        students.add(new Student("95001","zs","张三"));
        students.add(new Student("95002","ls","李四"));
        students.add(new Student("95003","ww","王五"));
        // 往水果集合中添加水果对象
        fruits.add(new Fruit("苹果","红色"));
        fruits.add(new Fruit("橘子","橙色"));
        fruits.add(new Fruit("香蕉","黄色"));

        // Key
        final String sKey = "redis:hashmap:1";
        final String fKey = "redis:hashmap:2";

        // 获取Hash存储操作组件hashOperations,遍历获取集合中的对象并添加到缓存中
        HashOperations hashOperations = redisTemplate.opsForHash();
        for (Student s:students) {
            hashOperations.put(sKey,s.getId(),s);
        }

        for (Fruit f:fruits) {
            hashOperations.put(fKey,f.getName(),f);
        }

        // 获取学生对象列表与水果对象列表
        Map<String,Student> sMap = hashOperations.entries(sKey);
        logger.info("获取学生对象列表:{}",mapper.writeValueAsString(sMap));
        Map<String,Fruit> fMap = hashOperations.entries(fKey);
        logger.info("获取水果对象列表:{}",mapper.writeValueAsString(fMap));

        // 获取指定的学生对象
        String sField = "95001";
        Student s = (Student) hashOperations.get(sKey,sField);
        logger.info("获取指定的学生对象:{}->{}",sField,mapper.writeValueAsString(s));
        // 获取指定的水果对象
        String fField = "苹果";
        Fruit f = (Fruit) hashOperations.get(fKey,fField);
        logger.info("获取指定的水果对象:{}->{}",sField,mapper.writeValueAsString(s));
    }

}

日志:

2024-02-05 16:37:15.266  INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController      : 获取学生对象列表:{"95003":{"id":"95003","userName":"ww","name":"王五"},"95001":{"id":"95001","userName":"zs","name":"张三"},"95002":{"id":"95002","userName":"ls","name":"李四"}}
2024-02-05 16:37:15.270  INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController      : 获取水果对象列表:{"橘子":{"name":"橘子","color":"橙色"},"香蕉":{"name":"香蕉","color":"黄色"},"苹果":{"name":"苹果","color":"红色"}}
2024-02-05 16:37:15.274  INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController      : 获取指定的学生对象:95001->{"id":"95001","userName":"zs","name":"张三"}
2024-02-05 16:37:15.277  INFO 21008 --- [nio-9090-exec-1] c.a.redis.controller.HashController      : 获取指定的水果对象:苹果->{"name":"苹果","color":"红色"}

总结: Redis的Hash存储特别适用于具有映射关系的对象存储。在实际互联网应用,当需要存入缓存中的对象信息具有某种共性时, 为了减少缓存中Key的数量,应考虑采用Hash哈希存储。

Redis中Key失效与判断是否存在

由于Redis本质上是一个基于内存、Key-Value存储的数据库,因而不管采用何种数据类型存储数据时,都需要提供一个Key,称为“键”,用来作为缓存数据的唯一标识。 而获取缓存数据的方法正是通过这个Key来获取到对应的信息,这一点在上面几部分中以代码实战Redis各种数据类型时已有所体现。 然而在某些业务场景下,缓存中的Key对应的数据信息并不需要永久保留,这个时候就需要对缓存中的这些Key进行“清理”。 在Redis缓存体系结构中,Delete与Expire操作都可以用于清理缓存中的Key,这两者不同之处在于Delete操作需要人为手动触发,而Expire只需要提供一个TTL,即“过期时间”,就可以实现Key的自动失效,也就是自动被清理。 下面的代码演示如何使缓存中的Key失效。 方式1:在调用SETEX方法中指定Key的过期时间

@GetMapping("/one")
    public void one() throws Exception {

        final String key1 = "redis:ttl:1";

        ValueOperations valueOperations = redisTemplate.opsForValue();
        // 第一种方式:在往redis中set数据时,提供一个TTL,表示ttl时间一到,缓存中的key自动失效,即被清理,在这里ttl是10秒
        valueOperations.set(key1, "expire操作", 10L, TimeUnit.SECONDS);
        // 等待5秒判断key是否还存在
        Thread.sleep(5000);

        Boolean existKey1 = redisTemplate.hasKey(key1);

        Object value = valueOperations.get(key1);

        logger.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey1, value);

        //再等待5秒-再判断key是否还存在
        Thread.sleep(5000);
        existKey1 = redisTemplate.hasKey(key1);
        value = valueOperations.get(key1);
        logger.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey1, value);

    }

日志:

2024-02-06 08:12:29.391  INFO 20940 --- [nio-9090-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-02-06 08:12:29.391  INFO 20940 --- [nio-9090-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-02-06 08:12:29.395  INFO 20940 --- [nio-9090-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
2024-02-06 08:12:44.505  INFO 20940 --- [nio-9090-exec-9] c.a.redis.controller.TtlController       : 等待5秒-判断key是否还存在:true 对应的值:expire操作
2024-02-06 08:12:49.529  INFO 20940 --- [nio-9090-exec-9] c.a.redis.controller.TtlController       : 再等待5秒-再判断key是否还存在:false 对应的值:null

从上述代码运行结果可以看出,当缓存中的Key失效时,对应的值也将不存在,即获取的值为null。

方式2:采用RedisTemplate操作组件的Expire()方法指定失效的Key

@GetMapping("/two")
    public void two() throws Exception {

        final String key2 = "redis:ttl:2";
        ValueOperations valueOperations=redisTemplate.opsForValue();
        //第二种方法:在往缓存中set数据后,采用redisTemplate的expire方法使该key失效
        valueOperations.set(key2, "expire操作-2");
        redisTemplate.expire(key2,10L, TimeUnit.SECONDS);
        //等待5秒-判断key是否还存在
        Thread.sleep(5000);
        Boolean existKey=redisTemplate.hasKey(key2);
        Object value=valueOperations.get(key2);
        logger.info("等待5秒-判断key是否还存在:{} 对应的值:{}", existKey, value);
        //再等待5秒-再判断key是否还存在
        Thread.sleep(5000);
        existKey=redisTemplate.hasKey(key2);
        value=valueOperations.get(key2);
        logger.info("再等待5秒-再判断key是否还存在:{} 对应的值:{}", existKey, value);

    }

日志:

2024-02-06 08:14:51.708  INFO 20940 --- [nio-9090-exec-2] c.a.redis.controller.TtlController       : 等待5秒-判断key是否还存在:true 对应的值:expire操作-2
2024-02-06 08:14:56.715  INFO 20940 --- [nio-9090-exec-2] c.a.redis.controller.TtlController       : 再等待5秒-再判断key是否还存在:false 对应的值:null

使缓存中的Key失效与判断 Key是否存在,在实际业务场景中是很常用的,最常见的场景包括:

  1. 将数据库查询到的数据缓存一定的时间 TTL,在 TTL时间内前端查询访问数据列表时,只需要在缓存中查询即可,从而减轻数据库的查询压力。
  2. 将数据压入缓存队列中,并设置一定的TTL时间,当TTL时间一到,将触发监听事件,从而处理相应的业务逻辑。

Redis缓存穿透

Redis缓存中间件确实能大大的提高应用程序的整体性能和效率。但是同时也带来了一些问题,比较典型的问题有: 缓存穿透 缓存雪崩 缓存击穿

本篇主要介绍缓存穿透的解决方案

什么是缓存穿透

简而言之:永远越过了缓存直接永远地访问数据库。

说明:

正常情况:
前端用户访问数据,后端首先从redis中查询,如果能查询到数据,则直接返回给用户,
如果缓存中没有数据,则从数据库中查询出来后,直接返回给用户,同时插入到缓存中,
如果在数据库中没有查询到数据,则返回null,同时流程结束。

这个正常查询的流程看上去没有问题,但最后一步"在数据库中没有查询到数据,则返回null,同时流程结束。"
如果前端频繁发起请求,恶意提供数据库中不存在的key,则此时数据库中查询到的数据将永远为null,由于null的数据是不
存入缓存中的,因而每次访问请求时将查询数据库,如果此时有恶意攻击,发起大量的请求,则很有可能对数据库造成极大压力
甚至压垮数据库,这就是缓存穿透

解决方案

当查询数据库时如果没有查询到数据,则将null返回给前端用户,同时将null数据插入到缓存中,并对对应的key设置一定 的过期时间,流程结束。

这种方案:在一定程度上可以减少数据库频繁查询的压力

实战案例

本案例采用springboot+redis,以"商城用户访问热销的商品"为实战案例 演示缓存穿透

步骤1:创建数据库脚本

CREATE TABLE `item` (
  `id` bigint(20) NOT NULL,
  `code` varchar(255) DEFAULT NULL COMMENT '商品编码',
  `name` varchar(255) DEFAULT NULL COMMENT '商品名称',
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

步骤2:创建实体类

@Table(name = "item")
public class Item {

    private long id;

    private String code;

    private String name;

    /**
     * 创建时间
     */
    @Column(name = "create_time")
    private Date createTime;


    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

步骤3:创建Mapper

public interface ItemMapper extends Mapper<Item> {

}

步骤4:创建Service

@Service
public class ItemService {

    private static final Logger log = LoggerFactory.getLogger(ItemService.class);

    @Autowired
    private ItemMapper itemMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    private static final String keyPrefix = "item:";

    /***
     * 获取商品详情,如果缓存有,则从缓存中获取,
     * 没有,查询数据库
     * @return
     * @throws Exception
     */
    public Item getItemInfo(String code) throws Exception {

        Item item = null;
        // 定义缓存中真正的key:由前缀和商品编码组成
        final String key = keyPrefix+code;
        ValueOperations valueOperations = redisTemplate.opsForValue();
        if (redisTemplate.hasKey(key)) {

            log.info("获取商品详情-缓存中的该商品编码为:{}"+code);
            // 从缓存中获取该商品详情
            Object res = valueOperations.get(key);
            if (res != null && !"".equals(res.toString())) {
                // 如果找到该商品,则进行反序列化解析
                item = objectMapper.readValue(res.toString(),Item.class);
            }
        } else {
            // 缓存中没有找到该商品
            log.info("获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}"+code);

            Item param = new Item();
            param.setCode(code);
            item = itemMapper.selectOne(param);
            if (item != null) {
                // 如果数据库中查询得到该商品,则将其序列化后写入缓存
                valueOperations.set(key,objectMapper.writeValueAsString(item));
            } else {
                // 过期失效时间设置为5秒,实际情况根据实际业务决定
                valueOperations.set(key,"",5, TimeUnit.SECONDS);
            }
        }
        return item;
    }
}

步骤5:创建Controller

@RestController
@RequestMapping(value = "/pass")
public class CachePassController {

    @Autowired
    private ItemService itemService;

    @GetMapping("/item/info")
    public Item itemInfo(String code) throws Exception{
        return itemService.getItemInfo(code);
    }

}

体验效果

2024-02-19 15:23:54.047  INFO 18240 --- [nio-9090-exec-2] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:54.239  INFO 18240 --- [nio-9090-exec-3] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:54.430  INFO 18240 --- [nio-9090-exec-6] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:54.629  INFO 18240 --- [nio-9090-exec-5] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}95002
2024-02-19 15:23:54.630 DEBUG 18240 --- [nio-9090-exec-5] c.a.redis.mapper.ItemMapper.selectOne    : ==>  Preparing: SELECT code,name,create_time FROM item WHERE code = ? 
2024-02-19 15:23:54.630 DEBUG 18240 --- [nio-9090-exec-5] c.a.redis.mapper.ItemMapper.selectOne    : ==> Parameters: 95002(String)
2024-02-19 15:23:54.631 DEBUG 18240 --- [nio-9090-exec-5] c.a.redis.mapper.ItemMapper.selectOne    : <==      Total: 0
2024-02-19 15:23:54.813  INFO 18240 --- [nio-9090-exec-4] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:58.833  INFO 18240 --- [nio-9090-exec-7] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.014  INFO 18240 --- [io-9090-exec-10] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.181  INFO 18240 --- [nio-9090-exec-9] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.357  INFO 18240 --- [nio-9090-exec-8] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.532  INFO 18240 --- [nio-9090-exec-1] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:23:59.701  INFO 18240 --- [nio-9090-exec-2] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}95002
2024-02-19 15:23:59.702 DEBUG 18240 --- [nio-9090-exec-2] c.a.redis.mapper.ItemMapper.selectOne    : ==>  Preparing: SELECT code,name,create_time FROM item WHERE code = ? 
2024-02-19 15:23:59.702 DEBUG 18240 --- [nio-9090-exec-2] c.a.redis.mapper.ItemMapper.selectOne    : ==> Parameters: 95002(String)
2024-02-19 15:23:59.703 DEBUG 18240 --- [nio-9090-exec-2] c.a.redis.mapper.ItemMapper.selectOne    : <==      Total: 0
2024-02-19 15:23:59.878  INFO 18240 --- [nio-9090-exec-3] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:00.068  INFO 18240 --- [nio-9090-exec-6] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:03.302  INFO 18240 --- [nio-9090-exec-5] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:03.621  INFO 18240 --- [nio-9090-exec-4] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:03.860  INFO 18240 --- [nio-9090-exec-7] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95002
2024-02-19 15:24:11.509  INFO 18240 --- [io-9090-exec-10] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中不存在该商品编码-从数据库中查询商品编码为:{}95002
2024-02-19 15:24:11.509 DEBUG 18240 --- [io-9090-exec-10] c.a.redis.mapper.ItemMapper.selectOne    : ==>  Preparing: SELECT code,name,create_time FROM item WHERE code = ? 
2024-02-19 15:24:11.509 DEBUG 18240 --- [io-9090-exec-10] c.a.redis.mapper.ItemMapper.selectOne    : ==> Parameters: 95002(String)
2024-02-19 15:24:11.510 DEBUG 18240 --- [io-9090-exec-10] c.a.redis.mapper.ItemMapper.selectOne    : <==      Total: 0
2024-02-19 15:24:48.297  INFO 18240 --- [nio-9090-exec-9] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95001
2024-02-19 15:29:49.439  INFO 18240 --- [nio-9090-exec-1] com.airycode.redis.service.ItemService   : 获取商品详情-缓存中的该商品编码为:{}95001

什么是缓存雪崩

缓存雪崩:指的是在某个时间点,缓存中的Key集体发生过期失效致使大量查询数据库的请求都落在了DB(数据库)上,导致数据库负载过高,压力暴增,甚至有可能“压垮”数据库。

这种问题产生的原因其实主要是因为大量的Key在某个时间点或者某个时间段过期失效导致的。 所以为了更好地避免这种问题的发生,一般的做法是为这些Key设置不同的、随机的TTL(过期失效时间),从而错开缓存中 Key的失效时间点,可以在某种程度上减少数据库的查询压力。

什么是缓存击穿

指缓存中某个频繁被访问的Key(可以称为“热点Key”),在不停地扛着前端的高并发请求,当这个Key突然在某个瞬间过期失效时,持续的高并发访问请求就“穿破”缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。 这种现象就像是“在一张薄膜上凿出了一个洞”。

这个问题产生的原因主要是热点的Key过期失效了,而在实际情况中,既然这个Key可以被当作“热点”频繁访问,那么就应该设置这个Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。

总结

不管是缓存穿透、缓存雪崩还是缓存击穿,其实它们最终导致的后果几乎都是一样的, 即给DB(数据库)造成压力,甚至压垮数据库。而它们的解决方案也都有一个共性, 那就是“加强防线”,尽量让高并发的读请求落在缓存中,从而避免直接跟数据库打交道。