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