57_Redis与Springboot的集合应用

news2024/11/16 11:46:21

前提

要实现,使用Redis存储登录状态

需要一个完整的前端后端的项目

前端项目搭建

  • 解压脚手架
  • 安装依赖
  • 配置请求代理
  • 选做: 禁用EsLint语法检查 Vue Admin Template关闭eslint校验,lintOnSave:false设置无效解决办法_lintonsave: false-CSDN博客

后端项目搭建

  • 创建springboot项目

  • 从其他项目中拷贝需要的依赖

  • 从其他项目拷贝所需的yml配置

  • 创建所需的entity,Controller,service,mapper,util

  • 写一个登录测试即可

七、与SpringBoot整合

7.1 RedisTemplate了解

spring-data-redis的jar中,提供在srping应用中通过简单的配置访问redis服务的功能,它对reids底层开发包进行了高度封装。

针对reids的操作,包中提供了RedisTemplate类和StringRedisTemplate类,其中StringRedisTemplate是RedisTemplate的子类,该类只支持key和value为String的操作

RedisTemplate针对不同数据类型的操作进行封装,将同一类型操作封装为Operation接口

  • ValueOperations:简单K-V操作,获取方式 redisTemplate.opsForValue();
  • SetOperations:set类型数据操作,获取方式 redisTemplate.opsForSet();
  • ZSetOperations:zset类型数据操作,获取方式 redisTemplate.opsForZSet();
  • HashOperations:针对hash类型的数据操作, 获取方式 redisTemplate.opsForHash();
  • ListOperations:针对list类型的数据操作,获取方式 redisTemplate.opsForList();

序列化策略

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

  • RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
  • GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
  • Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的
  • JacksonJsonRedisSerializer: 序列化object对象为json字符串
  • JdkSerializationRedisSerializer: 序列化java对象(被序列化的对象必须实现Serializable接口)
  • StringRedisSerializer: 简单的字符串序列化
  • GenericToStringSerializer:类似StringRedisSerializer的字符串序列化
  • GenericJackson2JsonRedisSerializer:类似Jackson2JsonRedisSerializer,但使用时构造函数不用特定的类

7.2 整合

7.2.1 依赖

        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- pool 对象池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

7.2.2 yml配置

spring:
  datasource:
    # 这里是之前mysql的....
  redis:
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

7.2.3 redis配置类

SpringBoot自动在容器中创建了RedisTemplate对象和StringRedisTemplate对象。但是,RedisTemplate的泛型是<Object,Object>,进行数据处理时比价麻烦,我们需要自定义一个RedisTemplate对象


ps: [了解]在SpringBoot 1.5.x版本默认的Redis客户端是Jedis实现的,SpringBoot 2.x版本默认客户端是用lettuce实现的

package com.qf.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 * 针对redis的配置类
 * 主要目的,设置RedisTemplate的序列化策略
 */
@Configuration
public class RedisConfig {

    @Autowired
    private LettuceConnectionFactory lettuceConnectionFactory;

    // 容器中默认的对象名是方法名
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        //key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        return redisTemplate;
    }
}

7.3 测试

记得测试依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

测试代码

package com.qf.test;

import com.qf.entity.StudentTb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 所有springboot 相关单元测试类 都必须在启动类所在包及其子包下
 */
@SpringBootTest // 作用就是标记当前类 一个springboot测试 ,可以启动springboot应用 并从容器中获取 容器中的bean
public class RedisTest {

    /**
     * 从容器中获取 redisTemplate
     *   redisTemplate 使用了模板设计模式,作用提供了统一的api 操作
     */
    @Autowired
    private RedisTemplate redisTemplate ;

    /**
     * @Test 表示当前方法是一个测试方法
     * 测试方法要求:  1.必须是public void
     *               2.无参
     * 测试value 为String 类型
     */
    @Test
    public void stringTest(){

        // valueOperations 就是一个专门用于操作 值为String 类型的redis工具
        // 相当于 redis 命令的 set   get
        ValueOperations valueOperations = redisTemplate.opsForValue();
        // set  a1 1000
        valueOperations.set("a1","1000h");
        valueOperations.set("a2","哈哈哈");
        //  get  a1
        Object result = valueOperations.get("a1");
        Object result2 = valueOperations.get("a2");
        System.out.println("result = " + result);
        System.out.println("result2 = " + result2);
    }

    /**
     * 操作list 数据
     *
     * @Data
     * public class StudentTb {
     *
     *     private int id;
     *     private String name;
     *     private int age;
     * }
     */
    @Test
    public void listTest(){

        StudentTb studentTb1 = new StudentTb();
        studentTb1.setId(1000);
        studentTb1.setName("xiaoming");
        studentTb1.setAge(18);

        StudentTb studentTb2 = new StudentTb();
        studentTb2.setId(1001);
        studentTb2.setName("lisi");
        studentTb2.setAge(28);

        // listOperations 专门用于操作redis中 的List 数据结构
        ListOperations listOperations = redisTemplate.opsForList();

        // 在redis key studentList 中添加数据 studentTb1对象
        listOperations.leftPush("studentList",studentTb1);
        listOperations.leftPush("studentList",studentTb2);

        // 从list 集合中读取数据  studentList
        List<StudentTb> studentList = listOperations.range("studentList", 0, -1);
        System.out.println("studentList = " + studentList);

    }

    /**
     * 操作 hash类型的数据
     *   存储对象
     */
    @Test
    public void hashTest(){
        // hashOperations 操作数据类型为 hash的数据
        HashOperations hashOperations = redisTemplate.opsForHash();

        hashOperations.put("stu1","id","1000");
        hashOperations.put("stu1","name","xiaoming");
        hashOperations.put("stu1","age","18");

        // 读取hash 类型中的数据
        String name = (String) hashOperations.get("stu1", "name");
        System.out.println("name = " + name);

    }

    /**
     * 测试  Set 类型数据
     */
    @Test
    public void setTest(){

        StudentTb studentTb1 = new StudentTb();
        studentTb1.setId(1000);
        studentTb1.setName("xiaoming");
        studentTb1.setAge(18);

        StudentTb studentTb2 = new StudentTb();
        studentTb2.setId(1001);
        studentTb2.setName("lisi");
        studentTb2.setAge(28);

        // setOperations 用于操作set 类型数据
        SetOperations setOperations = redisTemplate.opsForSet();
        setOperations.add("studentSet1",studentTb1,studentTb2);

        // 读取到studentSet1 对应的内容
        Set<StudentTb> studentSet1 = setOperations.members("studentSet1");

        System.out.println("studentSet1 = " + studentSet1);
    }

    /**
     * 测试 zset 数据类型
     */
    @Test
    public void  zSetTest(){
        StudentTb studentTb1 = new StudentTb();
        studentTb1.setId(1000);
        studentTb1.setName("xiaoming");
        studentTb1.setAge(18);

        StudentTb studentTb2 = new StudentTb();
        studentTb2.setId(1001);
        studentTb2.setName("lisi");
        studentTb2.setAge(28);

        // zSetOperations 专门用于操作zset
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        zSetOperations.add("zset1",studentTb1,88);
        zSetOperations.add("zset1",studentTb2,78);

        // 从zset中读取数据
        Set<StudentTb> zset1 = zSetOperations.range("zset1", 0, -1);
        System.out.println("zset1 = " + zset1);
    }

    /**
     * 操作key 相关命令
     */
    @Test
    public void  keyTest(){

        // 删除对应的key
        Boolean result = redisTemplate.delete("a2");
        System.out.println("result = " + result);

        // 设置a1 最多存活 10s
        redisTemplate.expire("a1",10, TimeUnit.MICROSECONDS);
    }
}

7.4 工具类

一般在开发的时候,不会直接使用RedisTemplate操作Redis

都会再封装一个工具类RedisUtil,类似下面这种(CV Ruoyi项目的)

image-20240725191652331

package com.qf.util;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;


/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc Redis工具类
 */
@Component
public class RedisUtil {

    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue( ).set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue( ).set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue( );
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList( ).rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList( ).range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator( );
        while (it.hasNext( )) {
            setOperation.add(it.next( ));
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet( ).members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash( ).putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash( ).entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash( ).put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash( );
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hKey
     */
    public void delCacheMapValue(final String key, final String hKey) {
        HashOperations hashOperations = redisTemplate.opsForHash( );
        hashOperations.delete(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash( ).multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

演示使用即可:

// 这里只是演示了取值动作…

@SpringBootTest
public class RedisTest {

    @Autowired
    private RedisUtil redisUtil ;

    @Test
    public void stringTestByUtil(){
        Object a1 = redisUtil.getCacheObject("a1");
        System.out.println("a1 = " + a1);

        Map<String, Object> stu1 = redisUtil.getCacheMap("stu1");
        System.out.println("stu1 = " + stu1);

        List<Object> studentList = redisUtil.getCacheList("studentList");
        System.out.println("studentList = " + studentList);

        Set<Object> studentSet1 = redisUtil.getCacheSet("studentSet1");
        System.out.println("studentSet1 = " + studentSet1);

    }
}

八、Redis应用

8.1 存储登录状态

8.1.1 分析

需求: 实现用户没有登录时不可访问以及每1小时登录一次

image-20240730141711633

思路:

  • 用户登录成功后,将用户信息存储到Redis中

    • 生成一个token当做key,用户信息当做value,并设置过期时间1小时
  • 并将这个token返回给前端

  • 前端登录成功后,从返回数据中取出token,存储到Vuex和Cookie中(Vue-admin-template架子是这么做的)

  • 后续前端每次发请求时,都会在请求头中携带这个token到后端

  • 后端设置拦截器,对接收的每个请求判断有无token

    • 无token说明没有登录,响应回前端让其重新登录
    • 有token,但是通过token从Redis中取不出数据,说明过期了,响应回前端让其重新登录
      • 至此: 思考一下,如何响应给前端让其重新登录? 前端后端要统一使用JSON交互(即统一返回对象R)的,拦截器中如何返回R?
      • 方案: 使用自定义异常类+全局异常处理
      • 思路: 拦截器中返回指定异常类,然后全局异常处理类中捕获这些异常,统一返回指定的状态码即可
      • 状态码多少? Vue-admin-template架子中设置了50008,50012,50014状态码
        • 50008: Illegal token;
        • 50012: Other clients logged in;
        • 50014: Token expired;
    • 有token,通过token从Redis中取出数据,则放行

总结: 整体思路就是: 登录时生成令牌,给到前端,前端每次携带令牌,后端对请求拦截实现鉴权

8.1.2 设置自定义异常类

设置一个没有登录异常类即可

package com.qf.ex;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc 未登录异常
 */
public class NoLoginException extends RuntimeException{

    // 为了接收状态码
    private int code;

    public int getCode() {
        return code;
    }

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

    public NoLoginException(int code,String message){
        super(message);
        this.code = code;
    }
}

8.1.3 设置全局异常处理

package com.qf.util;

import com.qf.ex.NoLoginException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc 自定义全局异常处理类
 */
@RestControllerAdvice
public class GlobalHandleException {

    @ExceptionHandler(NoLoginException.class)
    public R handlerException(Exception ex){
        System.out.println("出错啦!" + ex.getMessage());
        NoLoginException noLoginException = (NoLoginException) ex;
        
        // 返回状态码和错误信息
        return R.fail(noLoginException.getCode(),noLoginException.getMessage());
    }
}

8.1.4 登录时存储token

   @Autowired
    private RedisUtil redisUtil;

    @PostMapping("/login")
    public R login(@RequestBody SysUser sysUser) {

        SysUser user = service.login(sysUser);
        if (user != null) {
            // 1 登录成功,生成令牌
            String token = UUID.randomUUID( ).toString( ).replace("-", "");
            // 2 已令牌为key,对象信息为value存储到redis
            // key形如: user:34j34h53j4hj36
            // key形如: user:56j747b65756lk
            // value是对象,已经配置value使用jackson2Json将对象转成JSON字符串
            redisUtil.setCacheObject("user:"+token,user,1, TimeUnit.MINUTES);

            HashMap<String, String> map = new HashMap<>( );
            // 3 将令牌返回前端
            map.put("token", token);
            return R.ok(map);
        }
        return R.fail( );
    }

8.1.5 设置拦截器

@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisUtil redisUtil;

    // 登录成功后,将token发送给前端
    // 前端发送请求时,需要将token放到请求头中,发送给后台

    // 本例,从Authorization这个请求头中获取token值
    // 注意,需要将前端的请求头改变为Authorization
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        if (token == null || "".equals(token)) {
            throw new NoLoginException(50008,"无效令牌,重新登录");
        }
        SysUser sysUser = redisUtil.getCacheObject("user:" + token);
        if (sysUser == null) {
            throw new NoLoginException(50014,"身份信息失效,重新登录");
        }
        return true;
    }
}

别忘了配置拦截器

package com.qf.config;

import com.qf.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * --- 天道酬勤 ---
 *
 * @author QiuShiju
 * @desc
 */
@Configuration // 这个注解,让springboot框架知道,以下的这个类是提供配置
public class MyWebMvcInterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/sys/user/login");// 登录放行
    }
}

8.1.6 退出时销毁

/**
 * 退出登录
 */
@PostMapping("/logout")
public R logout(HttpServletRequest request) {
    request.getSession().invalidate();
    String token = request.getHeader("Authorization");
    
    // 销毁redis中的token
    redisUtil.deleteObject("user:"+token);
    return R.ok(  );
}

// ====================== 或者如下也行,不过得改前端  =======================
@GetMapping("/logout")
public R logout(String token) {
    redisUtil.deleteObject("user:"+token);
    return R.ok( );
}

8.1.7 请求测试

需要将请求头中的key修改为"Authorization"

image-20240727155656887

使用接口工具和网页测试即可

8.2 存储手机验证码

略。。。

  • 前端设置输入框,按钮绑定事件,点击发请求到后端
  • 后端接收请求后,调用工具类(短信工具类),生成验证码,存Redis一份(设置过期时间5分钟),短信发一份
  • 收到短信后,输入验证码
  • 发请求,输入的验证码要和后端Redis中的验证码比较
    • 输入的验证码与Redis中的验证码不一致,验证码错了
    • 输入的验证码,Redis中没有验证码,说明过期了
    • 如果正常,返回

8.3 如何保证数据在数据库和redis缓存的一致性

方案1:先删除缓存,再处理数据库 [不推荐]

1 a用户 执行删除数据操作,先删除缓存,还没有执行删除数据库时

2 b用户 执行查询操作,发现缓存中没有数据,查询数据库中老的数据,将数据放入缓存

3 a用户 执行删除数据库的操作,这时,缓存和数据库数据不一致了

而且只要缓存没有过期,只要没有其他的修改数据库的操作,缓存和数据库会长时间不一致

方案2:先操作数据库,再删除缓存 [推荐]

1 a用户删除数据库,还没有删除缓存前

2 b用户查询数据,从缓存中获取老的数据,这时候缓存和数据库不一致

3 a用户删除缓存

4 c用户请求数据,发现缓存中数据不存在,查询数据库新数据,将数据写入缓存,这时,缓存中是最新数据

实现缓存和数据库的数据短时间不一致,只有b出现一次不一致的情况,影响小

其他方案:延迟双删等

a先删除缓存,操作数据库,间隔一定的时间,a再删除一次缓存

九、Redis缓存的面试问题

【Redis】什么是Redis缓存 雪崩、穿透、击穿?(一篇文章就够了)_redis 雪崩-CSDN博客

  • 缓存穿透: 查询一个根本就不存在的数据
  • 缓存击穿: 查询一个之前存在,但是现在过期了的数据
  • 缓存雪崩: Redis中大量时间过期销毁
  • 缓存倾斜

缓存穿透问题

缓存穿透

  • 问题:查询一个不存在的数据,由于缓存中没有该数据,导致每次请求都会去数据库查询,数据库压力增大。

  • 解决方案

    • 布隆过滤器:在缓存之前先通过布隆过滤器判断数据是否存在,如果不存在则直接返回,避免访问数据库。

      什么是布隆过滤器?如何使用?-腾讯云开发者社区-腾讯云 (tencent.com)

      利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。
      
    • 缓存空值或默认值:对于不存在的数据,也在缓存中保存一个空值或默认值,并设置较短的过期时间,减少数据库查询压力。

    • 引入风控系统,对于频繁查询不存在的数据的请求进行限制或封禁。

image-20240725194948486

缓存击穿问题

缓存击穿: 本来缓存中有对应的数据,但是缓存的数据 因为到期,需要去数据库中再次查询数据

  • 问题:当某个热点数据在缓存中过期或者不存在时,大量请求会直接访问数据库,导致数据库压力骤增。
  • 解决方案
    • 使用互斥锁(如分布式锁)来控制只有一个请求去数据库加载数据,其他请求等待。
    • 逻辑过期,不直接设置过期时间,而是用程序逻辑判断数据是否“过期”,减少因过期导致缓存击穿的情况。
    • 预先加载,对于热点数据,在其过期前主动进行加载,避免过期时刻的并发访问。

image-20240729195922461

缓存雪崩问题

缓存雪崩问题:当缓存中大量的key 同时失效,此时大量的请求就会 穿过缓存层到达数据库,此时就会对数据库造成很大压力,数据库压力过大也会崩溃 ,此时就是缓存雪崩,由于缓存的失效 造成一系列的崩溃

缓存雪崩

  • 问题:当大量缓存数据同时过期或被删除时,大量请求会直接访问数据库,导致数据库压力骤增。
  • 解决方案
    • 添加随机过期时间:在设置缓存过期时间时,添加一定的随机时间,避免大量数据同时过期。
    • 使用分布式锁:在查询数据库时,使用分布式锁来避免并发查询导致的数据库压力增大。
    • 延迟双删策略:在更新数据时,先删除缓存中的数据,然后更新数据库。在更新数据库成功后,再次删除缓存中的数据,确保数据一致性。
    • 监控和告警:对Redis缓存系统进行监控和告警,及时发现和解决数据一致性问题。
缓存雪崩
image-20230913225454069

缓存倾斜问题

缓存倾斜

  • 问题:某个热点数据被大量请求访问,导致该数据所在的Redis节点压力过大,甚至可能引发宕机。
  • 解决方案
    • 热点数据分散:将热点数据分散到多个Redis节点中,避免单一节点压力过大。
    • 使用多级缓存:除了Redis缓存外,还可以引入其他缓存层(如本地缓存、CDN等),将热点数据缓存到离用户更近的地方,减少Redis的访问压力。
    • 热点数据预处理:对于热点数据,可以提前进行预处理和计算,减少实时计算的压力。
    • 监控和告警:对热点数据的访问进行监控和告警,及时发现并解决潜在问题。
缓存倾斜
image-20230913225540701
过期时间时,添加一定的随机时间,避免大量数据同时过期。
  • 使用分布式锁:在查询数据库时,使用分布式锁来避免并发查询导致的数据库压力增大。
  • 延迟双删策略:在更新数据时,先删除缓存中的数据,然后更新数据库。在更新数据库成功后,再次删除缓存中的数据,确保数据一致性。
  • 监控和告警:对Redis缓存系统进行监控和告警,及时发现和解决数据一致性问题。
缓存雪崩
image-20230913225454069

缓存倾斜问题

缓存倾斜

  • 问题:某个热点数据被大量请求访问,导致该数据所在的Redis节点压力过大,甚至可能引发宕机。
  • 解决方案
    • 热点数据分散:将热点数据分散到多个Redis节点中,避免单一节点压力过大。
    • 使用多级缓存:除了Redis缓存外,还可以引入其他缓存层(如本地缓存、CDN等),将热点数据缓存到离用户更近的地方,减少Redis的访问压力。
    • 热点数据预处理:对于热点数据,可以提前进行预处理和计算,减少实时计算的压力。
    • 监控和告警:对热点数据的访问进行监控和告警,及时发现并解决潜在问题。
缓存倾斜
image-20230913225540701

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1963579.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++和R穿刺针吸活检肿瘤算法模型模拟和进化动力学量化差异模型

&#x1f3af;要点 &#x1f3af;模拟肿瘤细胞增生进化轨迹 | &#x1f3af;肿瘤生长的随机空间细胞自动机模型 | &#x1f3af;模拟穿刺活检的收集空间局部的肿瘤块&#xff0c;模拟针吸活检采集长而薄的组织样本 | &#x1f3af;构建不同参数模拟合成肿瘤测试集 | &#x1f…

【TS】TypeScript函数类型:提升函数的类型安全性和可读性

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 TypeScript函数类型&#xff1a;提升函数的类型安全性和可读性1. 引言2. 基本函…

vue3 基于elementui el-table封装 hooks (未使用ts版本)

elementui-table封装 hooks js-hooks import { ref, reactive, onMounted } from vue import { ElMessage, ElMessageBox, ElNotification } from element-plus /*** FileDescription: el-table 函数式组件hooks--实现一个表格的数据加载、分页、搜索、删除和导出等操作,* fu…

tesseract 图片识别引擎

什么是tesseract Tesseract是一款开源的光学字符识别&#xff08;OCR&#xff09;引擎&#xff0c;用于从图像中提取文本。它由Ray Smith在惠普实验室于1985年至1995年开发&#xff0c;并在2005年由惠普将其作为开源软件发布。之后&#xff0c;Tesseract的开发由Google主导&am…

如何理解低代码?怎么用好低代码

怎样理解低代码 理解一个概念最好的办法就是找相关概念进行类比&#xff0c;那么想要单独理解低代码这个概念是不太行的。我们还需要了解“零代码”以及“纯代码”开发的概念。那么什么是低代码&#xff1f;其与“零代码”和“纯代码”的关系又是如何&#xff1f; 零代码 也许…

IDEA资源文件中文乱码问题解决方法

目录 一、问题描述二、解决方法 一、问题描述 IDEA中打开资源文件出现乱码。 二、解决方法 进入“Settings” -> “Editor” -> “File Encodings”&#xff0c;按下图修改箭头所指的三个地方&#xff1a;

java类加载和双亲委派及加载恶意类代码应用

前面的CC1和CC6&#xff0c;都是在Runtime.exec执行命令。如果WAF过滤了Runtime就寄&#xff0c;而且用命令的方式写入shell进行下一步利用&#xff0c;在流量中一个数据包就能把你的行为全部看完&#xff0c;很容易被分析出来。 如果用恶意字节码加载的方式&#xff0c;我们的…

轮询系统的具体原理是什么

一般市面上的轮询系统&#xff0c;看似高深莫测&#xff0c;但都是大差不差的&#xff0c;没有太多的技术含量&#xff0c;都是通过你的订单情况&#xff0c;在数据库中找出一个可以收这个订单的支付方式&#xff0c;然后经过b站来实现与支付通道商交互。 这个过程看似复杂&am…

IP-GUARD文档云备份服务器迁移数据操作说明

一、功能简介 使用文档云备份过程可能出现需要迁移旧数据到新目录的情况(如一开始存储目录设置 不合理,之后变更存储目录),下面介绍迁移备份数据到新目录的方法,迁移后可正常查看、 下载、删除原备份文件。 二、同一计算机上迁移存储目录 当仅需要将存储目录迁移到同一计…

随机森林的可解释性分析(含python代码)

随机森林的可解释性分析 1.引言可解释性的重要性 2.随机森林的原理2.1 基本原理&#xff1a;2.2 随机森林的实现 3.随机森林的可解释性分析3.1 特征重要性3.2 特征重要性3.3 SHAP值3.4 部分依赖图&#xff08;PDP&#xff09;3.5 交互特征效应3.6 变量依赖图 4.结论5.参考文献 …

Android和iOS 测试五个最好的开源自动化工具

本文主要介绍Android和iOS 五个最好的开源自动化工具&#xff0c;这里整理了相关资料&#xff0c;希望能帮助测试软件的朋友&#xff0c;有需要的看下 自动化测试在产品测试上有着非常重要的作用。实现测试自动化有多种积极的方式&#xff0c;包括最大限度地减少测试执行时间&…

昇思25天学习打卡营第26天|munger85

ShuffleNet图像分类 和mobilenet一样&#xff0c;也是在资源有限的设备上进行神经网络来做ai图像分类的小模型&#xff0c;在保持精度的同时大大降低了模型的计算量。 是基本块 就是真正的网络&#xff0c;如果模型size是2&#xff0c;就是输出的时候多一些&#xff0c;精细一…

公司监控员工电脑都能监控到什么?公司电脑可以监控到哪些内容?

很多人反馈&#xff0c;公司监控电脑&#xff1a; 那到底为什么安装监控&#xff1f; 公司监控员工电脑又都能监控到什么&#xff1f; 下面小编跟你细细道来~ 第一部分&#xff1a;架空员工电脑监控的目的 在现代企业管理中&#xff0c;电脑监控已成为一种常见的做法。 企…

【LLM大模型】落地RAG系列:RAG入门及RAG面临的挑战和解决方案!!

2023 年以来&#xff0c;RAG 已成为基于大模型的人工智能系统中应用最为广泛的架构之一。因此对 RAG 应用的性能、检索效率、准确性的研究成为核心问题。 本文首先介绍什么是 RAG、为什需要 RAG、介绍 Naive RAG 工作流程及Naive RAG 存在的问题和挑战&#xff01; 为什么需要 …

10:WiFi模块服务器模式

WiFi模块服务器模式 1、单片机通过WiFi模块向移动设备进行通信 我们通过AT指令ATCWMODE x&#xff0c;可以配置WiFi模块的工作模式。   ATCWMODE1为移动设备模式&#xff0c;这时WiFi模块可以连接其他路由器WiFi&#xff0c;然后可以给连接这个WiFi的其他移动设备发送数据&a…

JAVA静态代理和动态代理

前言&#xff1a; 静态代理: 静态代理是在编译时就已经确定了代理类的具体实现。代理类需要实现与目标类相同的接口,并且持有目标对象的引用。在代理类中实现对目标方法的增强或修改。静态代理的优点是实现简单,可以很好地控制目标对象的行为。缺点是每个目标对象都需要创建一…

从光速常数的可变性看宇宙大爆炸的本质

基于先前关于光速本质的讨论&#xff0c;让我们从函数图像看看宇宙大爆炸到底是什么。 先前已经讨论过&#xff0c;在量子尺度上&#xff0c;长度的实际对应物是频率的差异&#xff0c;因为只有频率差异才能在这个尺度上区分相邻时空的两点&#xff0c;而两点之间“差异的大小”…

再不怕数据丢失了!全量增量的迁移工具发布!

随着用户量的增加,我们收到了各种各样的需求反馈。 为了更好地拓展Chat2DB Pro 产品, 我们很高兴地宣布推出了插件市场功能, 同时重磅推出数据迁移工具DBMotion插件。 &#x1f680; 关于 DBMotion 插件 DBMotion插件&#xff0c;是一款基于沃趣科技的 DBMotion 数据迁移工具…

软件兼容性测试内容和步骤简析,湖南软件测评公司分享

软件兼容性测试是确保软件产品在不同的硬件环境、操作系统、浏览器和设备上正常运行的重要环节。随着科技的迅猛发展&#xff0c;各类软件应用层出不穷&#xff0c;用户对软件的多样性需求日益增加&#xff0c;软件的兼容性显得尤为重要。 软件兼容性测试内容包含多个方面&…

RocketMQ中的参数约束和建议

消息发送重试次数&#xff1a; 默认值&#xff1a;3次。&#xff08;取值范围&#xff1a;无限制&#xff09; 消息发送重试和流控机制 | RocketMQ 消息消费重试次数&#xff1a; 默认值&#xff1a;16次。