redis与springBoot整合

前提

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

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

前端项目搭建

后端项目搭建

  • 创建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项目的)

复制代码
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小时登录一次

思路:

  • 用户登录成功后,将用户信息存储到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"

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

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 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。
    • 缓存空值或默认值:对于不存在的数据,也在缓存中保存一个空值或默认值,并设置较短的过期时间,减少数据库查询压力。

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

缓存击穿问题

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

  • 问题:当某个热点数据在缓存中过期或者不存在时,大量请求会直接访问数据库,导致数据库压力骤增。

  • 解决方案

    • 使用互斥锁(如分布式锁)来控制只有一个请求去数据库加载数据,其他请求等待。

    • 逻辑过期,不直接设置过期时间,而是用程序逻辑判断数据是否"过期",减少因过期导致缓存击穿的情况。

    • 预先加载,对于热点数据,在其过期前主动进行加载,避免过期时刻的并发访问。

缓存雪崩问题

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

缓存雪崩

  • 问题:当大量缓存数据同时过期或被删除时,大量请求会直接访问数据库,导致数据库压力骤增。

  • 解决方案

    • 添加随机过期时间:在设置缓存过期时间时,添加一定的随机时间,避免大量数据同时过期。

    • 使用分布式锁:在查询数据库时,使用分布式锁来避免并发查询导致的数据库压力增大。

    • 延迟双删策略:在更新数据时,先删除缓存中的数据,然后更新数据库。在更新数据库成功后,再次删除缓存中的数据,确保数据一致性。

    • 监控和告警:对Redis缓存系统进行监控和告警,及时发现和解决数据一致性问题。

缓存雪崩

缓存倾斜问题

缓存倾斜

  • 问题:某个热点数据被大量请求访问,导致该数据所在的Redis节点压力过大,甚至可能引发宕机。

  • 解决方案

    • 热点数据分散:将热点数据分散到多个Redis节点中,避免单一节点压力过大。

    • 使用多级缓存:除了Redis缓存外,还可以引入其他缓存层(如本地缓存、CDN等),将热点数据缓存到离用户更近的地方,减少Redis的访问压力。

    • 热点数据预处理:对于热点数据,可以提前进行预处理和计算,减少实时计算的压力。

    • 监控和告警:对热点数据的访问进行监控和告警,及时发现并解决潜在问题。

相关推荐
Code apprenticeship2 小时前
怎么利用Redis实现延时队列?
数据库·redis·缓存
百度智能云技术站2 小时前
广告投放系统成本降低 70%+,基于 Redis 容量型数据库 PegaDB 的方案设计和业务实践
数据库·redis·oracle
装不满的克莱因瓶2 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
黄名富6 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
G_whang7 小时前
centos7下docker 容器实现redis主从同步
redis·docker·容器
.生产的驴7 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
我叫啥都行10 小时前
计算机基础复习12.22
java·jvm·redis·后端·mysql
阿乾之铭11 小时前
Redis四种模式在Spring Boot框架下的配置
redis
on the way 12313 小时前
Redisson锁简单使用
redis
科马14 小时前
【Redis】缓存
数据库·redis·spring·缓存