---
title: "16-缓存"
created: 2025-12-02
tags:
- 项目
aliases:
- 缓存
---
# 缓存
## 分析
主页需要展示一些推荐内容 比如推荐用户 推荐商品……
如果一股脑将mysql查询的结果拉取到前端页面
数据量一大 浏览器会崩溃
为了防止崩溃 可以加上一层分页查询 只在前端展示一页的结果 避免一次性返回大量数据
另外 毕竟涉及到查询 如果数据量一大 哪怕是用了分页 查询也是需要时间的 也就导致前端的响应比较慢
如果响应时间超过一秒 就很影响用户体验
解决方法是
换一个比数据库更快的地方做查询 同操作系统优化思路一样 从数据库查其实就是从硬盘里取 优化方法就是放入内存中或者缓存中
加上一层缓存 redis 常用的数据无需查询数据库 直接从缓存中取
用了缓存也会出现一个问题 如果没缓存的时候 查询就特别慢 有缓存查询特别快 给人一种随缘的感觉
如果只有用户访问时才填充缓存 第一次访问的数据加载会很慢
为了防止第一个人查询特别慢的情况出现
可以加上一个[[定时任务]] 在低峰期(用户量较少时)预加载数据到缓存
可以将常用的推荐内容或商品信息在空闲时间提前加载 保证用户在高峰期时能够获得快速响应
**后续优化策略**
**缓存失效与更新策略**
- 为避免缓存内容过时 可以设置合适的缓存失效时间(TTL,Time-To-Live)
另外 也可以在数据库更新时触发缓存的更新或删除 确保缓存中的数据始终是最新的
- 可以考虑使用**缓存穿透**机制,例如使用`Bloom Filter`来判断数据是否存在于缓存中,避免查询到不存在的缓存项。
**异步加载与懒加载**
- 如果页面展示的数据量较大,可以考虑使用异步加载技术,让推荐内容在用户滚动或交互时动态加载,而不是一开始就加载全部内容。
**性能监控与分析**
- 使用监控工具(如Prometheus、Grafana等)定期查看数据库查询、缓存命中率、前端响应时间等性能指标,及时发现瓶颈,优化系统。
### 缓存分类
分布式缓存:
- Redis(分布式缓存)
- memcached(分布式)
- Etcd(云原生架构的一个分布式存储, **存储配置** ,扩容能力)
单机缓存:
- ehcache
- Java 内存集合,如 HashMap
- Caffeine(Java 内存缓存性能之王,高性能)
- Google Guava|
## redis
### 概念
缓存
> NoSQL 数据库
key - value 存储系统(区别于 MySQL,他存储的是键值对)
#### Redis 数据结构
基本:
- String 字符串类型: name: "Zwww"
- List 列表:names: ["Zwww", "zv04", "Zv"]
- Set 集合:names: ["Zwww", "zv04"](值不能重复)
- Hash 哈希:nameAge: { "Zwww": 1, "zv04": 2 }
- Zset 集合:names: { Zwww - 9, zv04 - 12 }(适合做排行榜)
高级:
- bloomfilter(布隆过滤器,主要从大量的数据中快速过滤值,比如邮件黑名单拦截)
- geo(计算地理位置)
- hyperloglog(pv / uv)
- pub / sub(发布订阅,类似消息队列)
- BitMap (1001010101010101010101010101)
### 引入
引入依赖
```xml
org.springframework.boot
spring-boot-starter-data-redis
2.6.4
```
配置redis地址
```yaml
spring:
# redis 配置
redis:
port: 6379
host: localhost
database: 1
```
### 测试
```java
package com.zwnsyw.backend.service;
import com.zwnsyw.backend.pojo.User;
import org.junit.jupiter.api.Assertions;
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.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
@SpringBootTest
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
void test(){
ValueOperations valueOperations = redisTemplate.opsForValue();
// 增
valueOperations.set("ZwwwString", "cat");
valueOperations.set("ZwwwInt", 1);
valueOperations.set("ZwwwDouble", 2.0);
User user = new User();
user.setId(1L);
user.setUserName("Zwww");
valueOperations.set("ZwwwUser", user);
// 查
Object Zwww = valueOperations.get("ZwwwString");
Assertions.assertTrue("cat".equals((String) Zwww));
Zwww = valueOperations.get("ZwwwInt");
Assertions.assertTrue(1 == (Integer) Zwww);
Zwww = valueOperations.get("ZwwwDouble");
Assertions.assertTrue(2.0 == (Double) Zwww);
System.out.println(valueOperations.get("ZwwwUser"));
valueOperations.set("ZwwwString", "cat");
redisTemplate.delete("ZwwwString");
}
}
```
![[image-8d0dc03e.png]]
但在quickredis中 key不存在 为乱码
![[image-1a6789c3.png]]
是因为redisTemplate的配置默认是以jdk序列化的 我们看上去就是乱码
需要写一个配置类 配置成json格式的序列化
#### 自定义序列化
为了防止写入 Redis 的数据乱码、浪费空间等,可以自定义序列化器。
com.zwnsyw.backend.config.RedisTemplateConfig
```java
package com.zwnsyw.backend.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.zwnsyw.backend.common.ErrorCode;
import com.zwnsyw.backend.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.RedisConnectionFailureException;
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.RedisSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* RedisTemplate 配置
*
* @author Zwww
*/
@Configuration
@EnableRedisHttpSession
@Slf4j
public class RedisTemplateConfig {
@Value("${redis.host:localhost}")
private String redisHost;
@Value("${redis.port:6379}")
private int redisPort;
/**
* 配置 RedisTemplate 使用 Jackson 序列化
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
GenericJackson2JsonRedisSerializer genericSerializer = genericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(genericSerializer);
redisTemplate.setDefaultSerializer(genericSerializer);
redisTemplate.setEnableTransactionSupport(true); // 开启事务支持
try {
redisTemplate.afterPropertiesSet();
} catch (RedisConnectionFailureException e) {
log.error("Redis 连接失败:主机:{},端口:{}", redisHost, redisPort);
throw new BusinessException(ErrorCode.DATABASE_ERROR, "连接 Redis 失败");
} catch (Exception e) {
log.error("Redis 配置失败:主机:{},端口:{}", redisHost, redisPort);
throw new BusinessException(ErrorCode.DATABASE_ERROR, "Redis 配置出错");
}
log.info("Redis 配置成功,主机:{},端口:{}", redisHost, redisPort);
return redisTemplate;
}
/**
* 通用 JSON 序列化器
*/
private GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
// 创建 ObjectMapper 并配置类型安全
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.zwnsyw") // 仅允许业务相关类
.allowIfSubType("com.zwnsyw.backend.pojo.*") // 添加业务对象所在的包
.allowIfSubType("com.baomidou.mybatisplus.extension.plugins.pagination.Page") // 添加 MyBatisPlus 分页类
.allowIfSubType("java.util.ArrayList") // 允许的集合类型:ArrayList
.allowIfSubType("java.util.HashMap") // 允许的集合类型:HashMap
.allowIfSubType("java.util.LinkedList") // 可根据需要添加更多集合或类型
.allowIfSubType("java.util.TreeMap")
.allowIfSubType("java.util.LinkedHashMap")
.allowIfSubType("java.lang.String") // 基本类型支持
.allowIfSubType("java.lang.Long")
.allowIfSubType("java.lang.Integer")
.allowIfSubType("java.lang.Double")
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
}
```
> 引入一个新库时,记得先写测试类
![[image-03faba22.png]]
#### session序列化为json
虽然配置了RedisTemplateConfig 但登入信息还是以乱码存在redis里面的
配置application.yml
```yaml
spring:
# session 配置
session:
store-type: redis
redis:
namespace: session # 设置 Redis 中存储 session 数据的命名空间
flush-mode: on_save # 表示当 session 更新时,才会将数据保存到 Redis
timeout: 86400 # session 过期时间:24小时
# 在RedisTemplateConfig中 使用@EnableRedisHttpSession注解
# 表示用自定义的 RedisTemplate 来存储 session 数据
# 确保 session 数据以 JSON 格式存储在 Redis 中 而不是默认的 JDK 序列化 显示为乱码
```
另外注意
RedisHttpSessionConfiguration中Bean名称必须是springSessionDefaultRedisSerializer才能指定Jackson序列化器
修改RedisTemplateConfig:
```java
package com.zwnsyw.backend.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.zwnsyw.backend.common.ErrorCode;
import com.zwnsyw.backend.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.RedisConnectionFailureException;
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.RedisSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* RedisTemplate 配置
*
* @author Zwww
*/
@Configuration
@EnableRedisHttpSession
@Slf4j
public class RedisTemplateConfig {
@Value("${redis.host:localhost}")
private String redisHost;
@Value("${redis.port:6379}")
private int redisPort;
/**
* 配置 RedisTemplate 使用 Jackson 序列化
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
GenericJackson2JsonRedisSerializer genericSerializer = genericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(genericSerializer);
redisTemplate.setDefaultSerializer(genericSerializer);
redisTemplate.setEnableTransactionSupport(true); // 开启事务支持
try {
redisTemplate.afterPropertiesSet();
} catch (RedisConnectionFailureException e) {
log.error("Redis 连接失败:主机:{},端口:{}", redisHost, redisPort);
throw new BusinessException(ErrorCode.DATABASE_ERROR, "连接 Redis 失败");
} catch (Exception e) {
log.error("Redis 配置失败:主机:{},端口:{}", redisHost, redisPort);
throw new BusinessException(ErrorCode.DATABASE_ERROR, "Redis 配置出错");
}
log.info("Redis 配置成功,主机:{},端口:{}", redisHost, redisPort);
return redisTemplate;
}
/**
* 通用 JSON 序列化器
*/
private GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
// 创建 ObjectMapper 并配置类型安全
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.zwnsyw") // 仅允许业务相关类
.allowIfSubType("com.zwnsyw.backend.pojo.*") // 添加业务对象所在的包
.allowIfSubType("com.baomidou.mybatisplus.extension.plugins.pagination.Page") // 添加 MyBatisPlus 分页类
.allowIfSubType("java.util.ArrayList") // 允许的集合类型:ArrayList
.allowIfSubType("java.util.HashMap") // 允许的集合类型:HashMap
.allowIfSubType("java.util.LinkedList") // 可根据需要添加更多集合或类型
.allowIfSubType("java.util.TreeMap")
.allowIfSubType("java.util.LinkedHashMap")
.allowIfSubType("java.lang.String") // 基本类型支持
.allowIfSubType("java.lang.Long")
.allowIfSubType("java.lang.Integer")
.allowIfSubType("java.lang.Double")
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
/**
* Spring Session 使用的 Redis 序列化器
*/
@Bean
@Qualifier("springSessionDefaultRedisSerializer")
public RedisSerializer