一,使用缓存的必要性
二,redis作为缓存的优势
三,redis作为缓存的一般逻辑图
四,redis整合SpringBoot使用的两种方式
4.1,纯代码手工实现
4.1.1,创建maven项目并导入相关的mysql,redis依赖
4.1.2,配置yml文件
4.1.3,配置RedisTemplate模板
4.1.4,根据数据库构建相对应的实体类,在service层上编写相关业务。
4.1.5,测试
4.2,注解实现缓存
4.2.1,构建数据库并配置yml文件
4.2.2,配置缓存管理器
4.2.3,开启注解支持
4.2.4,SpringCache的一些相关注解说明
4.2.5,创建pojo类,编写dao与mapper.xml
4.2.6,编写业务接口及其实现类
4.2.7,编写controller层并进行测试
五,缓存中常见问题
5.1,缓存穿透
5.2,缓存击穿
5.3,缓存雪崩
一,使用缓存的必要性
当服务器端收到客户端请求量变多时,某些数据请求量大也会随之变大,这些热点数据要频繁的从数据库中读取,给数据库造成压力,自然会导致服务器响应客户端变慢。因此,在一些不考虑实时性的数据中,我们通常会将这些数据临时存储存在内存中,当请求时候,我们就能够直接读取内存中的数据及时响应。这就是使用缓存的初衷。
缓存主要用于解决高性能与高并发以时减少数据库压力的作用。它的本质就是将数据存储在内存中,当数据没有发生本质变化的时候,应尽量避免直接连接数据库进行查询,因为并发高时很可能会将数据库压塌,而是应去缓存中读取数据,只有缓存中未查找到时再去数据库中查询,这样就大大降低了数据库的读写次数,增加系统的性能和能提供的并发量。
二,redis作为缓存的优势
Redis提供了高性能的数据存储功能。单个Redis server对请求的处理是基于单线程工作模型的,但由于是纯内存操作,并且单线程的工作模式避免了线程上下文切换带来的额外开销,同时使用NIO多路复用机制(单线程维护多个I/O socket的状态,socket event handler统一进行event分发,并通知到各个event listener),所以即使是单台Redis server的性能也是非常的快,可支持11万次/秒的SET操作,8.1万次/秒的GET操作。这样可以减少网络的IO次数和数据体积。
Redis将其数据完全保存在内存中,仅使用磁盘进行持久化。与其它键值数据存储相比,Redis有一组相对丰富的数据类型。支持string,list,set,sorted set,hash
Redis支持数据持久化和数据恢复,支持master/slave主/从机制,sentinal哨兵模式以及cluster集群模式,允许单点故障,虽然同时也会付出性能的代价,但是相比于MemCached,其稳定性还是有保证的(MemCached不支持数据持久化,断电或重启后数据消失)
Redis支持事务,操作具有原子性 - 所有Redis操作都是原子操作,这确保如果两个客户端并发访问,Redis服务器能接收更新的值。
三,redis作为缓存的一般逻辑图
如下:
采用redis作为Mysql数据库的缓存,在查找的时候,首先查找redis缓存,如果找到则返回结果;如果在redis中没有找到,那么查找Mysql数据库,找到的花则返回结果并且更新redis;如果没有找到则返回空。对于写入的情况,直接写入mysql数据库,mysql数据库通过触发器及UDF机制自动把变更的内容更新到redis中。
四,redis整合SpringBoot使用的两种方式
4.1,纯代码手工实现
由于这种方法笔者比较复杂,代码量也多,因此并不推荐,只做简单实现,供大家了解一下即可。
4.1.1,创建maven项目并导入相关的mysql,redis依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/>
</parent>
<groupId>com.yy</groupId>
<artifactId>springboot-08-jedis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-08-jedis</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.1.2,配置yml文件
spring:
datasource:
username: root
password: 123456
#时区报错,需要添加时区
url: jdbc:mysql://localhost:3306/school?serverTimezone=UTC&useSSL=true&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
springboot-redis配置
spring:
redis:
host: redis地址
port: 6379
password: 123456
database: 0
4.1.3,配置RedisTemplate模板
从依赖项中我们可以看到,SpringBoot集成redis的依赖中为我们提供了redisTamplate类来对redis进行操作。
其中他为我们提供了默认的序列化方式 :JdkSerializationRedisSerializer,作为JDK提供的序列化功能。 这个序列化方式优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。并且这种默认序列化方式在RDM工具中查看k-v值时会出现“乱码”,不方便查看。
因此我们需要自己去自定义序列化方式来更好的方便我们操作。在此,我更推荐去配置Jackson2JsonRedisSerializer序列化方式,因为它是使用Jackson库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。
在redis配置类中配置自定义的序列化方式
package com.yy.myconfig;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
-
自定义一个redis template模板
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
//为了使开发方便,直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);//Json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //string的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //key采用string的序列化方式 template.setKeySerializer(stringRedisSerializer); //hash的key也是用string的序列化方式 template.setHashKeySerializer(stringRedisSerializer); //value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); //hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template;
}
}
为了方便我们对reids不同的数据类型的操作,有需要的同学也可以将redisTamplate封装为一个工具类,以便后面直接调用。
package com.yy.utils;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.Date;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisTemplate<String, Object> getRedisTemplate() {
return this.redisTemplate;
}
/** -------------------key相关操作--------------------- */
/**
* 删除key
*
* @param key
*/
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 批量删除key
*
* @param keys
*/
public void delete(Collection<String> keys) {
redisTemplate.delete(keys);
}
/**
* 序列化key
*
* @param key
* @return
*/
public byte[] dump(String key) {
return redisTemplate.dump(key);
}
/**
* 是否存在key
*
* @param key
* @return
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 设置过期时间
*
* @param key
* @param timeout
* @param unit
* @return
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 设置过期时间
*
* @param key
* @param date
* @return
*/
public Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}
/**
* 查找匹配的key
*
* @param pattern
* @return
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 将当前数据库的 key 移动到给定的数据库 db 当中
*
* @param key
* @param dbIndex
* @return
*/
public Boolean move(String key, int dbIndex) {
return redisTemplate.move(key, dbIndex);
}
/**
* 移除 key 的过期时间,key 将持久保持
*
* @param key
* @return
*/
public Boolean persist(String key) {
return redisTemplate.persist(key);
}
/**
* 返回 key 的剩余的过期时间
*
* @param key
* @param unit
* @return
*/
public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}
/**
* 返回 key 的剩余的过期时间
*
* @param key
* @return
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}
/**
* 从当前数据库中随机返回一个 key
*
* @return
*/
public String randomKey() {
return (String) redisTemplate.randomKey();
}
/**
* 修改 key 的名称
*
* @param oldKey
* @param newKey
*/
public void rename(String oldKey, String newKey) {
redisTemplate.rename(oldKey, newKey);
}
/**
* 仅当 newkey 不存在时,将 oldKey 改名为 newkey
*
* @param oldKey
* @param newKey
* @return
*/
public Boolean renameIfAbsent(String oldKey, String newKey) {
return redisTemplate.renameIfAbsent(oldKey, newKey);
}
/**
* 返回 key 所储存的值的类型
*
* @param key
* @return
*/
public DataType type(String key) {
return redisTemplate.type(key);
}
/** -------------------string相关操作--------------------- */
/**
* 设置指定 key 的值
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 获取指定 key 的值
*
* @param key
* @return
*/
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 返回 key 中字符串值的子字符
*
* @param key
* @param start
* @param end
* @return
*/
public String getRange(String key, long start, long end) {
return redisTemplate.opsForValue().get(key, start, end);
}
/**
* 将给定 key 的值设为 value ,并返回 key 的旧值(old value)
*
* @param key
* @param value
* @return
*/
public String getAndSet(String key, String value) {
return (String) redisTemplate.opsForValue().getAndSet(key, value);
}
/**
* 对 key 所储存的字符串值,获取指定偏移量上的位(bit)
*
* @param key
* @param offset
* @return
*/
public Boolean getBit(String key, long offset) {
return redisTemplate.opsForValue().getBit(key, offset);
}
/**
* 批量获取
*
* @param keys
* @return
*/
public List<Object> multiGet(Collection<String> keys) {
return redisTemplate.opsForValue().multiGet(keys);
}
/**
* 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
*
* @param key 位置
* @param value 值,true为1, false为0
* @return
*/
public boolean setBit(String key, long offset, boolean value) {
return redisTemplate.opsForValue().setBit(key, offset, value);
}
/**
* 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
*
* @param key
* @param value
* @param timeout 过期时间
* @param unit 时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
* 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
*/
public void setEx(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 只有在 key 不存在时设置 key 的值
*
* @param key
* @param value
* @return 之前已经存在返回false, 不存在返回true
*/
public boolean setIfAbsent(String key, String value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
/**
* 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
*
* @param key
* @param value
* @param offset 从指定位置开始覆写
*/
public void setRange(String key, String value, long offset) {
redisTemplate.opsForValue().set(key, value, offset);
}
/**
* 获取字符串的长度
*
* @param key
* @return
*/
public Long size(String key) {
return redisTemplate.opsForValue().size(key);
}
/**
* 批量添加
*
* @param maps
*/
public void multiSet(Map<String, String> maps) {
redisTemplate.opsForValue().multiSet(maps);
}
/**
* 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在
*
* @param maps
* @return 之前已经存在返回false, 不存在返回true
*/
public boolean multiSetIfAbsent(Map<String, String> maps) {
return redisTemplate.opsForValue().multiSetIfAbsent(maps);
}
/**
* 增加(自增长), 负数则为自减
*
* @param key
* @return
*/
public Long incrBy(String key, long increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* @param key
* @return
*/
public Double incrByFloat(String key, double increment) {
return redisTemplate.opsForValue().increment(key, increment);
}
/**
* 追加到末尾
*
* @param key
* @param value
* @return
*/
public Integer append(String key, String value) {
return redisTemplate.opsForValue().append(key, value);
}
/** -------------------hash相关操作------------------------- */
/**
* 获取存储在哈希表中指定字段的值
*
* @param key
* @param field
* @return
*/
public Object hGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
/**
* 获取所有给定字段的值
*
* @param key
* @return
*/
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 获取所有给定字段的值
*
* @param key
* @param fields
* @return
*/
public List<Object> hMultiGet(String key, Collection<Object> fields) {
return redisTemplate.opsForHash().multiGet(key, fields);
}
public void hPut(String key, String hashKey, String value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public void hPutAll(String key, Map<String, String> maps) {
redisTemplate.opsForHash().putAll(key, maps);
}
/**
* 仅当hashKey不存在时才设置
*
* @param key
* @param hashKey
* @param value
* @return
*/
public Boolean hPutIfAbsent(String key, String hashKey, String value) {
return redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
}
/**
* 删除一个或多个哈希表字段
*
* @param key
* @param fields
* @return
*/
public Long hDelete(String key, Object... fields) {
return redisTemplate.opsForHash().delete(key, fields);
}
/**
* 查看哈希表 key 中,指定的字段是否存在
*
* @param key
* @param field
* @return
*/
public boolean hExists(String key, String field) {
return redisTemplate.opsForHash().hasKey(key, field);
}
/**
* 为哈希表 key 中的指定字段的整数值加上增量 increment
*
* @param key
* @param field
* @param increment
* @return
*/
public Long hIncrBy(String key, Object field, long increment) {
return redisTemplate.opsForHash().increment(key, field, increment);
}
/**
* 为哈希表 key 中的指定字段的整数值加上增量 increment
*
* @param key
* @param field
* @param delta
* @return
*/
public Double hIncrByFloat(String key, Object field, double delta) {
return redisTemplate.opsForHash().increment(key, field, delta);
}
/**
* 获取所有哈希表中的字段
*
* @param key
* @return
*/
public Set<Object> hKeys(String key) {
return redisTemplate.opsForHash().keys(key);
}
/**
* 获取哈希表中字段的数量
*
* @param key
* @return
*/
public Long hSize(String key) {
return redisTemplate.opsForHash().size(key);
}
/**
* 获取哈希表中所有值
*
* @param key
* @return
*/
public List<Object> hValues(String key) {
return redisTemplate.opsForHash().values(key);
}
/**
* 迭代哈希表中的键值对
*
* @param key
* @param options
* @return
*/
public Cursor<Map.Entry<Object, Object>> hScan(String key, ScanOptions options) {
return redisTemplate.opsForHash().scan(key, options);
}
/** ------------------------list相关操作---------------------------- */
/**
* 通过索引获取列表中的元素
*
* @param key
* @param index
* @return
*/
public String lIndex(String key, long index) {
return (String) redisTemplate.opsForList().index(key, index);
}
/**
* 获取列表指定范围内的元素
*
* @param key
* @param start 开始位置, 0是开始位置
* @param end 结束位置, -1返回所有
* @return
*/
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 存储在list头部
*
* @param key
* @param value
* @return
*/
public Long lLeftPush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lLeftPushAll(String key, String... value) {
return redisTemplate.opsForList().leftPushAll(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lLeftPushAll(String key, Collection<String> value) {
return redisTemplate.opsForList().leftPushAll(key, value);
}
/**
* 当list存在的时候才加入
*
* @param key
* @param value
* @return
*/
public Long lLeftPushIfPresent(String key, String value) {
return redisTemplate.opsForList().leftPushIfPresent(key, value);
}
/**
* 如果pivot存在,再pivot前面添加
*
* @param key
* @param pivot
* @param value
* @return
*/
public Long lLeftPush(String key, String pivot, String value) {
return redisTemplate.opsForList().leftPush(key, pivot, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lRightPush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lRightPushAll(String key, String... value) {
return redisTemplate.opsForList().rightPushAll(key, value);
}
/**
* @param key
* @param value
* @return
*/
public Long lRightPushAll(String key, Collection<String> value) {
return redisTemplate.opsForList().rightPushAll(key, value);
}
/**
* 为已存在的列表添加值
*
* @param key
* @param value
* @return
*/
public Long lRightPushIfPresent(String key, String value) {
return redisTemplate.opsForList().rightPushIfPresent(key, value);
}
/**
* 在pivot元素的右边添加值
*
* @param key
* @param pivot
* @param value
* @return
*/
public Long lRightPush(String key, String pivot, String value) {
return redisTemplate.opsForList().rightPush(key, pivot, value);
}
/**
* 通过索引设置列表元素的值
*
* @param key
* @param index 位置
* @param value
*/
public void lSet(String key, long index, String value) {
redisTemplate.opsForList().set(key, index, value);
}
/**
* 移出并获取列表的第一个元素
*
* @param key
* @return 删除的元素
*/
public String lLeftPop(String key) {
return (String) redisTemplate.opsForList().leftPop(key);
}
/**
* 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
*
* @param key
* @param timeout 等待时间
* @param unit 时间单位
* @return
*/
public String lBLeftPop(String key, long timeout, TimeUnit unit) {
return (String) redisTemplate.opsForList().leftPop(key, timeout, unit);
}
/**
* 移除并获取列表最后一个元素
*
* @param key
* @return 删除的元素
*/
public String lRightPop(String key) {
return (String) redisTemplate.opsForList().rightPop(key);
}
/**
* 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
*
* @param key
* @param timeout 等待时间
* @param unit 时间单位
* @return
*/
public String lBRightPop(String key, long timeout, TimeUnit unit) {
return (String) redisTemplate.opsForList().rightPop(key, timeout, unit);
}
/**
* 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
*
* @param sourceKey
* @param destinationKey
* @return
*/
public String lRightPopAndLeftPush(String sourceKey, String destinationKey) {
return (String) redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
destinationKey);
}
/**
* 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
*
* @param sourceKey
* @param destinationKey
* @param timeout
* @param unit
* @return
*/
public String lBRightPopAndLeftPush(String sourceKey, String destinationKey,
long timeout, TimeUnit unit) {
return (String) redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
destinationKey, timeout, unit);
}
/**
* 删除集合中值等于value得元素
*
* @param key
* @param index index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素;
* index<0, 从尾部开始删除第一个值等于value的元素;
* @param value
* @return
*/
public Long lRemove(String key, long index, String value) {
return redisTemplate.opsForList().remove(key, index, value);
}
/**
* 裁剪list
*
* @param key
* @param start
* @param end
*/
public void lTrim(String key, long start, long end) {
redisTemplate.opsForList().trim(key, start, end);
}
/**
* 获取列表长度
*
* @param key
* @return
*/
public Long lLen(String key) {
return redisTemplate.opsForList().size(key);
}
/** --------------------set相关操作-------------------------- */
/**
* set添加元素
*
* @param key
* @param values
* @return
*/
public Long sAdd(String key, String... values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* set移除元素
*
* @param key
* @param values
* @return
*/
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
/**
* 移除并返回集合的一个随机元素
*
* @param key
* @return
*/
public String sPop(String key) {
return (String) redisTemplate.opsForSet().pop(key);
}
/**
* 将元素value从一个集合移到另一个集合
*
* @param key
* @param value
* @param destKey
* @return
*/
public Boolean sMove(String key, String value, String destKey) {
return redisTemplate.opsForSet().move(key, value, destKey);
}
/**
* 获取集合的大小
*
* @param key
* @return
*/
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 判断集合是否包含value
*
* @param key
* @param value
* @return
*/
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 获取两个集合的交集
*
* @param key
* @param otherKey
* @return
*/
public Set<Object> sIntersect(String key, String otherKey) {
return redisTemplate.opsForSet().intersect(key, otherKey);
}
/**
* 获取key集合与多个集合的交集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sIntersect(String key, Collection<String> otherKeys) {
return redisTemplate.opsForSet().intersect(key, otherKeys);
}
/**
* key集合与otherKey集合的交集存储到destKey集合中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long sIntersectAndStore(String key, String otherKey, String destKey) {
return redisTemplate.opsForSet().intersectAndStore(key, otherKey,
destKey);
}
/**
* key集合与多个集合的交集存储到destKey集合中
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long sIntersectAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForSet().intersectAndStore(key, otherKeys,
destKey);
}
/**
* 获取两个集合的并集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sUnion(String key, String otherKeys) {
return redisTemplate.opsForSet().union(key, otherKeys);
}
/**
* 获取key集合与多个集合的并集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sUnion(String key, Collection<String> otherKeys) {
return redisTemplate.opsForSet().union(key, otherKeys);
}
/**
* key集合与otherKey集合的并集存储到destKey中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long sUnionAndStore(String key, String otherKey, String destKey) {
return redisTemplate.opsForSet().unionAndStore(key, otherKey, destKey);
}
/**
* key集合与多个集合的并集存储到destKey中
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long sUnionAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 获取两个集合的差集
*
* @param key
* @param otherKey
* @return
*/
public Set<Object> sDifference(String key, String otherKey) {
return redisTemplate.opsForSet().difference(key, otherKey);
}
/**
* 获取key集合与多个集合的差集
*
* @param key
* @param otherKeys
* @return
*/
public Set<Object> sDifference(String key, Collection<String> otherKeys) {
return redisTemplate.opsForSet().difference(key, otherKeys);
}
/**
* key集合与otherKey集合的差集存储到destKey中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long sDifference(String key, String otherKey, String destKey) {
return redisTemplate.opsForSet().differenceAndStore(key, otherKey,
destKey);
}
/**
* key集合与多个集合的差集存储到destKey中
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long sDifference(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForSet().differenceAndStore(key, otherKeys,
destKey);
}
/**
* 获取集合所有元素
*
* @param key
* @return
*/
public Set<Object> setMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 随机获取集合中的一个元素
*
* @param key
* @return
*/
public String sRandomMember(String key) {
return (String) redisTemplate.opsForSet().randomMember(key);
}
/**
* 随机获取集合中count个元素
*
* @param key
* @param count
* @return
*/
public List<Object> sRandomMembers(String key, long count) {
return redisTemplate.opsForSet().randomMembers(key, count);
}
/**
* 随机获取集合中count个元素并且去除重复的
*
* @param key
* @param count
* @return
*/
public Set<Object> sDistinctRandomMembers(String key, long count) {
return redisTemplate.opsForSet().distinctRandomMembers(key, count);
}
/**
* @param key
* @param options
* @return
*/
public Cursor<Object> sScan(String key, ScanOptions options) {
return redisTemplate.opsForSet().scan(key, options);
}
/**------------------zSet相关操作--------------------------------*/
/**
* 添加元素,有序集合是按照元素的score值由小到大排列
*
* @param key
* @param value
* @param score
* @return
*/
public Boolean zAdd(String key, String value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
/**
* @param key
* @param values
* @return
*/
public Long zAdd(String key, Set<ZSetOperations.TypedTuple<Object>> values) {
return redisTemplate.opsForZSet().add(key, values);
}
/**
* @param key
* @param values
* @return
*/
public Long zRemove(String key, Object... values) {
return redisTemplate.opsForZSet().remove(key, values);
}
/**
* 增加元素的score值,并返回增加后的值
*
* @param key
* @param value
* @param delta
* @return
*/
public Double zIncrementScore(String key, String value, double delta) {
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
/**
* 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
*
* @param key
* @param value
* @return 0表示第一位
*/
public Long zRank(String key, Object value) {
return redisTemplate.opsForZSet().rank(key, value);
}
/**
* 返回元素在集合的排名,按元素的score值由大到小排列
*
* @param key
* @param value
* @return
*/
public Long zReverseRank(String key, Object value) {
return redisTemplate.opsForZSet().reverseRank(key, value);
}
/**
* 获取集合的元素, 从小到大排序
*
* @param key
* @param start 开始位置
* @param end 结束位置, -1查询所有
* @return
*/
public Set<Object> zRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* 获取集合元素, 并且把score值也获取
*
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScores(String key, long start,
long end) {
return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
}
/**
* 根据Score值查询集合元素
*
* @param key
* @param min 最小值
* @param max 最大值
* @return
*/
public Set<Object> zRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
/**
* 根据Score值查询集合元素, 从小到大排序
*
* @param key
* @param min 最小值
* @param max 最大值
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key,
double min, double max) {
return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
}
/**
* @param key
* @param min
* @param max
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key,
double min, double max, long start, long end) {
return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max,
start, end);
}
/**
* 获取集合的元素, 从大到小排序
*
* @param key
* @param start
* @param end
* @return
*/
public Set<Object> zReverseRange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
/**
* 获取集合的元素, 从大到小排序, 并返回score值
*
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeWithScores(String key,
long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start,
end);
}
/**
* 根据Score值查询集合元素, 从大到小排序
*
* @param key
* @param min
* @param max
* @return
*/
public Set<Object> zReverseRangeByScore(String key, double min,
double max) {
return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
}
/**
* 根据Score值查询集合元素, 从大到小排序
*
* @param key
* @param min
* @param max
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeByScoreWithScores(
String key, double min, double max) {
return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
min, max);
}
/**
* @param key
* @param min
* @param max
* @param start
* @param end
* @return
*/
public Set<Object> zReverseRangeByScore(String key, double min,
double max, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max,
start, end);
}
/**
* 根据score值获取集合元素数量
*
* @param key
* @param min
* @param max
* @return
*/
public Long zCount(String key, double min, double max) {
return redisTemplate.opsForZSet().count(key, min, max);
}
/**
* 获取集合大小
*
* @param key
* @return
*/
public Long zSize(String key) {
return redisTemplate.opsForZSet().size(key);
}
/**
* 获取集合大小
*
* @param key
* @return
*/
public Long zZCard(String key) {
return redisTemplate.opsForZSet().zCard(key);
}
/**
* 获取集合中value元素的score值
*
* @param key
* @param value
* @return
*/
public Double zScore(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 移除指定索引位置的成员
*
* @param key
* @param start
* @param end
* @return
*/
public Long zRemoveRange(String key, long start, long end) {
return redisTemplate.opsForZSet().removeRange(key, start, end);
}
/**
* 根据指定的score值的范围来移除成员
*
* @param key
* @param min
* @param max
* @return
*/
public Long zRemoveRangeByScore(String key, double min, double max) {
return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
}
/**
* 获取key和otherKey的并集并存储在destKey中
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long zUnionAndStore(String key, String otherKey, String destKey) {
return redisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey);
}
/**
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long zUnionAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForZSet()
.unionAndStore(key, otherKeys, destKey);
}
/**
* 交集
*
* @param key
* @param otherKey
* @param destKey
* @return
*/
public Long zIntersectAndStore(String key, String otherKey,
String destKey) {
return redisTemplate.opsForZSet().intersectAndStore(key, otherKey,
destKey);
}
/**
* 交集
*
* @param key
* @param otherKeys
* @param destKey
* @return
*/
public Long zIntersectAndStore(String key, Collection<String> otherKeys,
String destKey) {
return redisTemplate.opsForZSet().intersectAndStore(key, otherKeys,
destKey);
}
/**
* @param key
* @param options
* @return
*/
public Cursor<ZSetOperations.TypedTuple<Object>> zScan(String key, ScanOptions options) {
return redisTemplate.opsForZSet().scan(key, options);
}
}
4.1.4,根据数据库构建相对应的实体类,在service层上编写相关业务。
dao层上的简单操作
package com.yy.service;
import com.yy.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface US {
@Select("select * from user where id=#{id}")
User selectUserById(Integer id);
}
业务类上进行实现
package com.yy.service;
import com.yy.pojo.User;
import com.yy.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ServiceImpl {
@Autowired
private US us;
@Autowired
RedisUtils redisUtils;
public User select(Integer id) {
if (Boolean.FALSE.equals(redisUtils.hasKey(String.valueOf(id)))) {
log.info("查询mysql数据库");
User user = us.selectUserById(id);
user.setId(id);
user.setUsername("张三");
user.setPwd("147852");
redisUtils.hPut("user", String.valueOf(id), String.valueOf(user));
return user;
} else {
log.info("查询redis");
return (User) redisUtils.hGet("user", String.valueOf(id));
}
}
}
4.1.5,测试
编写一个测试类进行测试:
@Test
void test() {
System.out.println(service.select(1002));
}
运行结果可以看到数据已经查询出来,并且将数据存储在redis中了
但是这种方式,代码量和操作都不太方便。因此,在项目中我跟推荐使用注解去实现。
4.2,注解实现缓存
因为导入依赖与上面大致一样,需要注意的是我在这个新的测试上使用了Mybatis-plus插件,所以依赖上还需要加上相关依赖,其他地方不重复详述。
4.2.1,构建数据库并配置yml文件
server:
port: 8090
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
password: 123456
username: root
url: jdbc:mysql://localhost:3306/db_user?&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
redis:
database: 0
port: 6379
host: redis的ip地址
timeout: 50000
lettuce:
pool:
max-active: 8 #连接池最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示无限制)
max-idle: 8 #最大空闲连接数
password: 123456
aop:
proxy-target-class: true
mybatis-plus:
mapper-locations: classPath:/mapper/*.xml
configuration:
map-underscore-to-camel-case: false # 禁止大写变小写时自动添加下划线
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.yy.pojo
为了测试一下缓存效果,这次笔者重新构建了一个数据库,并向数据库中导入10万条测试数据。以备后面测试缓存读取数据时的效果。
4.2.2,配置缓存管理器
package com.yy.redisCache.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
/**
* 配置缓存管理器
* @param factory 线程连接工厂
* @return 缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置缓存过期时间5分钟
config.entryTtl(Duration.ofMinutes(5))
//设置缓存前缀
.prefixCacheNameWith("redis:cache:")
//禁止缓存null值
.disableCachingNullValues()
//添加键值序列化配置
.serializeKeysWith(keyPair())
.serializeValuesWith(valuePair());
return RedisCacheManager.builder(factory)
.withCacheConfiguration("test",config)
.build();
}
/**
* 配置键序列化
* @return
*/
@Bean
RedisSerializationContext.SerializationPair<String> keyPair(){
return RedisSerializationContext.SerializationPair.fromSerializer( new StringRedisSerializer());
}
/**
* 配置值序列化
* @return
*/
@Bean
RedisSerializationContext.SerializationPair<Object> valuePair(){
return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
}
}
4.2.3,开启注解支持
在启动类上添加@EnableCache,开启缓存功能
package com.yy;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableCaching//开启缓存
@MapperScan({"com.yy.dao","com.yy.redisCache.dao"})
@EnableTransactionManagement
public class SpringbootMongodbStudyApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootMongodbStudyApplication.class, args);
}
}
4.2.4,SpringCache的一些相关注解说明
类型 名称 解释
注解
@CacheConfig
一般配置在指定类上用于指定缓存名称,并且名称与缓存管理器中的缓存名保持一致
@Cacheable 可标记在方法上,也可以标记在类上。标记在类上时表示该类的所有方法都支持缓存。标记在方法上时,该方法的返回结果将会进行缓存。如果已经存在该缓存,则会直接从缓存中读取数据。可以通过key指定入参,value为方法返回值缓存在得 Cache名称
@CachePut 可以标注在类上和方法上。无论是否存在缓存,每次都会重新添加缓存,常用于更新。
@CacheEvit 可以标记在一个方法上,也可以标记在一个类上。标记方法上标识方法执行时候触发清楚缓存操作,当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
@Caching 可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。
注解配置参数 value,cacheNames 指定(缓存管理器) Cache名称,效果类似于@CacheConfig中的指定名称。如果已经用@CacheConfig在类上指定了,方法中就不需要使用了,就像提取公共名称一样
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。未设置则为默认值。
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
unless 可以用来是决定是否添加到缓存,与 condition不同的是,unless表达式是在方法调用之后进行评估的。如果返回false,才放入缓存 (与condition相反)
sync 在多线程环境下,某些操作可能使用相同参数同步调用。默认情况下,缓存不锁定任何资源,可能导致多次计算,而违反了缓存的目的。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中
EL表达式可以使用方法参数及对应的属性。使用方法参数时,可以直接使用 "#参数名" 或者 "#p参数index9(参数下标)",#result为返回值
4.2.5,创建pojo类,编写dao与mapper.xml
package com.yy.redisCache.pojo;
import lombok.Data;
import java.io.Serializable;
/**
- @author young
- @date 2022/9/4 13:02
- @description:
*/
@Data
public class RedisTest implements Serializable {
private static final long serialVersionUID = -7120480143440693836L;
private Integer id;
private String redis_name;
private String redis_pwd;
private String address;
}
4.2.6,编写业务接口及其实现类
package com.yy.redisCache.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yy.redisCache.pojo.RedisTest;
/**
- @author young
- @date 2022/9/4 13:06
- @description: 业务接口
*/
public interface RedisTestService extends IService<RedisTest> {
}
在实现类中添加缓存注解,指定的业务的返回结果存入缓存。
package com.yy.redisCache.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yy.redisCache.dao.RedisTestDao;
import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.RedisTestService;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
@author young
@date 2022/9/4 13:08
-
@description: service实现类
*/
@Service
@CacheConfig(cacheNames = "test")//缓存名,和管理器中配置的一致
public class RedisTestServiceImpl extends ServiceImpl<RedisTestDao, RedisTest> implements RedisTestService {
@Resource
private RedisTestDao redisTestDao;/**
- 查寻10000条数据
- @return
- key 可为空,指定需按照SpEL编写
- unless 不缓存的条件 条件为true不缓存
*/
@Override
@Cacheable(key = "'allUser'",unless = "#result==null")//#result返回结果
public List<RedisTest> list() {
QueryWrapper<RedisTest> wrapper = new QueryWrapper<>();
wrapper.le("id",10000);
return redisTestDao.selectList(wrapper);
}
/**
* 批量查询
* @param idList
* @return
*/
@Override
public List<RedisTest> listByIds(Collection<? extends Serializable> idList) {
return super.listByIds(idList);
}
/**
* 删除批量信息
* @param list
* @return
*/
@CacheEvict
@Override
public boolean removeByIds(Collection<?> list) {
return super.removeByIds(list);
}
/**
* 修改某个对象
* @param entity
* @return
*/
@Override
@CachePut
public boolean updateById(RedisTest entity) {
return super.updateById(entity);
}
}
4.2.7,编写controller层并进行测试
package com.yy.redisCache.controller;
import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.impl.RedisTestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@author young
@date 2022/9/4 13:13
-
@description:
*/
@RestController
@RequestMapping("/Cache")
@Slf4j
public class RedisCacheController {
@Autowired
private RedisTestServiceImpl service;/**
- 删除指定id以下的所有数据及缓存
- @param id id
- @return
*/
@DeleteMapping("/del/{id}")
public Boolean remove(@PathVariable Integer id){
List<Integer> list = new ArrayList<>();
for (int i = 0; i <= id; i++) {
list.add(i);
}
return service.removeByIds(list);
}
/**
- 更新数据
- @param redisTest 更新对象
- @return
*/
@PutMapping("/update")
public Boolean update(RedisTest redisTest){
return service.updateById(redisTest);
}
/**
- 测试从数据库/缓存获取10000条数据的时间
- @return
*/
@GetMapping("/get")
public List<RedisTest> getBody(){
Long startTime = System.currentTimeMillis();
List<RedisTest> list = service.list();
//第一次存入数据库查询3875ms 第二次缓存查询:1131ms
log.info("查询时间10000条数据的时间是:{}",System.currentTimeMillis()-startTime);
return list;
}
/**
- 批量获取指定的数据
- @param id
- @return
*/
@GetMapping("/getOne/{id}")
public String getOne(@PathVariable Integer... id){
List<RedisTest> ids = service.listByIds(Arrays.asList(id));
return ids.isEmpty()?"获取成功":"获取失败";
}
}
通过接口测试工具测试获取10000条数据的测试后,可以看到第一次获取10000条数据后,时间为3875ms,并且redis里已经有了相关缓存。
再次执行该请求后,获取时间大大减少为1131ms。此时,表示是从缓存读取的数据。
可以猜想到,由于数据库执行查询操作时,需要进行数据库连接等操作,因次直接从数据库获取数据的时间会比redis缓存慢。最后我们测试一下更新删除缓存等操作,成功达到我们的预期结果。
五,缓存中常见问题
5.1,缓存穿透
缓存穿透是指查询一个不存在的数据,在缓存层和持久层都查询不到,这样查不到的数据不会写入缓存,这样一个不存在的数据每次的请求都会去查询持久层,缓存也就失去了保护后端持久层的意义了。最严重的后果就是是后端存储负载加大,造成后端存储宕机。
形成原因:
业务代码或数据出现问题,比如读写key不一致
网络爬虫或恶意攻击
解决方法:
1,缓存空对象:对于查询不到的数据给它设置一个空值。但是可能会造成内存空间紧张,因为value为null也会占用内存空间,不过我们可以给它设置一个过期时间,让空值缓存自动过期。这种方法使用与数据经常变化,实时性较高的场景。
2,布隆过滤器拦截:在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
5.2,缓存击穿
某个key成为一个热点(商品秒杀时)时,这样处于一个集中式高并发的情况下,如果key突然失效一瞬间,请求就会马上击穿缓存层,直接请求数据库,这样后端负载会很快过载甚至崩溃。
解决办法:
1,根据实际情况设置二级缓存或者热点缓存永不过期。
2,设置分布式互斥锁,只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据
5.3,缓存雪崩
当缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。
解决办法:
1,设置多级缓存,不同的缓存设置不同的过期时间。
2,将缓存层设计为高可用,例如使用sentinel哨兵模式或者cluster,这样即使个别节点或机器宕机也不会影响缓存效果实现。
3,将缓存的过期时间设置随机分布的,避免在同一时间内缓存集体失效。
————————————————
版权声明:本文为CSDN博主「深情不及里子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_42263280/article/details/126760489