一、为什么选择 Redis 作为 SpringBoot 的缓存方案?
在高并发业务场景下,数据库往往是性能瓶颈,而 Redis 作为高性能的内存数据库,是 SpringBoot 项目缓存的最优选择之一:
- 性能优势:Redis 基于内存操作,单机 QPS 可达 10 万 +,远超传统关系型数据库;
- 集群支持:支持主从、哨兵、集群模式,满足高可用生产环境需求;
- SpringBoot 原生适配 :通过
spring-boot-starter-data-redis快速集成,提供RedisTemplate和缓存注解双重操作方式; - 丰富的过期策略:支持键级别的过期时间,可灵活控制缓存生命周期;
- 序列化可控:可自定义序列化规则,解决 JDK 序列化乱码、时间类型解析等问题。
二、核心配置:Redis 集群 + 序列化 + 缓存管理器
1. Maven 依赖配置(pom.xml)
xml
<?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 http://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.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cnhis.iho</groupId>
<artifactId>demo-redis</artifactId>
<name>Demo :: Redis</name>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<main.basedir>${basedir}/..</main.basedir>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Redis 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring缓存抽象层(提供@Cacheable等注解) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.34</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
2. 应用配置(application.yml)
yaml
server:
port: 10088
spring:
# Redis 集群配置
redis:
timeout: 6000ms
password: ${REDIS_PASSWORD:123123} # 环境变量优先,默认值123123
cluster:
max-redirects: 3 # 集群最大重定向次数
nodes: # 集群节点列表
- ${REDIS_NODE0:192.168.1.210:7000}
- ${REDIS_NODE1:192.168.1.210:7001}
- ${REDIS_NODE2:192.168.1.210:7002}
- ${REDIS_NODE3:192.168.1.210:7003}
- ${REDIS_NODE4:192.168.1.210:7004}
- ${REDIS_NODE5:192.168.1.210:7005}
lettuce: # Lettuce连接池(SpringBoot 2.x默认)
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接数
min-idle: 0 # 最小空闲连接数
max-wait: -1 # 连接等待时间(-1表示无限制)
cluster:
refresh:
adaptive: true # 自适应刷新集群节点信息
# 缓存配置:明确指定类型为Redis(增强可读性)
cache:
type: redis
3. 启动类:开启缓存注解
在启动类添加@EnableCaching,激活 Spring 缓存注解(@Cacheable/@CachePut/@CacheEvict):
java
package com.demo.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching // 开启缓存注解支持
@SpringBootApplication
public class DemoRedisApplication {
public static void main(String[] args) {
SpringApplication.run(DemoRedisApplication.class, args);
}
}
4. Redis 核心配置类(序列化 + 缓存管理器)
解决默认序列化乱码、时间类型解析问题,同时配置RedisCacheManager适配注解式缓存,复用序列化规则:
java
package com.demo.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
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.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Redis核心配置类
* 1. 自定义序列化规则(解决乱码、时间类型解析问题)
* 2. 配置RedisTemplate/StringRedisTemplate
* 3. 配置RedisCacheManager(适配注解式缓存)
*/
@Configuration
public class RedisConfigure {
// 全局序列化器:key用String,value用JSON(统一规则)
public static final RedisSerializer<String> KEY_SERIALIZER;
public static final RedisSerializer<Object> VALUE_SERIALIZER;
static {
KEY_SERIALIZER = new StringRedisSerializer();
VALUE_SERIALIZER = new GenericJackson2JsonRedisSerializer(getObjectMapper());
}
/**
* 自定义ObjectMapper:解决JDK8时间类型序列化/反序列化问题
*/
private static ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 开启所有字段可见性(包括private)
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 开启类型信息存储(反序列化时识别对象类型)
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 配置JDK8时间类型序列化规则
JavaTimeModule timeModule = new JavaTimeModule();
// LocalDate:yyyy-MM-dd
timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
// LocalDateTime:yyyy-MM-dd HH:mm:ss
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
// 关闭时间戳序列化(避免时间转数字)
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 忽略未知属性(反序列化时不报错)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 注册时间模块
objectMapper.registerModule(timeModule);
return objectMapper;
}
/**
* 自定义RedisTemplate:适配<Object, Object>类型操作
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 设置key/HashKey序列化器
redisTemplate.setKeySerializer(KEY_SERIALIZER);
redisTemplate.setHashKeySerializer(KEY_SERIALIZER);
// 设置value/HashValue序列化器
redisTemplate.setValueSerializer(VALUE_SERIALIZER);
redisTemplate.setHashValueSerializer(VALUE_SERIALIZER);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* StringRedisTemplate:适配<String, String>类型轻量操作
*/
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(redisConnectionFactory);
stringRedisTemplate.afterPropertiesSet();
return stringRedisTemplate;
}
/**
* RedisCacheManager:支撑注解式缓存,复用全局序列化规则
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 1. 基础缓存配置
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(KEY_SERIALIZER))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(VALUE_SERIALIZER))
.entryTtl(Duration.ofHours(1)) // 默认过期时间1小时
.disableCachingNullValues() // 不缓存null(防止缓存穿透)
.prefixCacheNameWith("demo:"); // 缓存key前缀(避免冲突)
// 2. 构建缓存管理器(支持不同缓存名自定义过期时间)
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(cacheConfig)
.withCacheConfiguration("user", cacheConfig.entryTtl(Duration.ofSeconds(10))) // user缓存10秒过期
.build();
}
}
三、实战开发:两种缓存使用方式
实现手动缓存(RedisTemplate) 和注解式缓存(@Cacheable) 两种方式,覆盖不同业务场景需求。
1. 定义 User 实体类
java
package com.demo.redis.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 用户实体类(实现Serializable兜底,实际使用JSON序列化)
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = 6643397313702951631L;
private Long id; // 用户ID
private String name; // 姓名
private Integer age; // 年龄
private String email; // 邮箱
private String phone; // 手机号
private String address; // 地址
private Integer gender; // 性别(1-男,0-女)
private String username; // 用户名
}
2. 定义业务接口
java
package com.demo.redis.service;
import com.demo.redis.dto.User;
/**
* 缓存演示业务接口
*/
public interface IDemoService {
// 手动缓存(RedisTemplate)
User getUser(Long userId);
// 注解式缓存:查询
User getUserById(Long userId);
// 注解式缓存:更新
User updateUser(User user);
// 注解式缓存:删除
void deleteUser(Long userId);
}
3. 业务实现类(核心)
java
package com.demo.redis.service.impl;
import com.demo.redis.dto.User;
import com.demo.redis.service.IDemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 缓存业务实现类
* 1. 手动缓存:通过RedisTemplate操作
* 2. 注解式缓存:通过@Cacheable/@CachePut/@CacheEvict
*/
@Slf4j
@Service
public class DemoServiceImpl implements IDemoService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 方式1:手动缓存(RedisTemplate)
* 适合需要精细化控制缓存的场景
*/
@Override
public User getUser(Long userId) {
String key = "user:" + userId;
// 1. 优先从缓存获取
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
log.info("从缓存获取用户:{}", userId);
return user;
}
// 2. 缓存未命中,查询数据库
user = queryDb(userId);
// 3. 存入缓存(30秒过期)
if (user != null) {
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);
log.info("用户数据存入缓存:{}", userId);
}
return user;
}
/**
* 方式2:注解式缓存(@Cacheable)
* 优先查缓存,无则查库并自动缓存
* cacheNames="user" → 最终key:demo:user:userId
*/
@Cacheable(cacheNames = "user", key = "#userId")
@Override
public User getUserById(Long userId) {
return queryDb(userId);
}
/**
* 注解式缓存:更新(@CachePut)
* 执行方法后自动更新缓存,保证缓存与数据一致
*/
@CachePut(cacheNames = "user", key = "#user.id")
@Override
public User updateUser(User user) {
log.info("更新用户缓存:{}", user.getId());
return user;
}
/**
* 注解式缓存:删除(@CacheEvict)
* 执行方法后自动清除对应缓存
*/
@CacheEvict(cacheNames = "user", key = "#userId")
@Override
public void deleteUser(Long userId) {
log.info("删除用户缓存:{}", userId);
}
/**
* 模拟数据库查询(实际项目替换为MyBatis/JPA)
*/
private User queryDb(Long userId) {
log.info("查询数据库:{}", userId);
User user = new User();
user.setId(userId);
user.setName("用户" + userId);
user.setAge(25);
user.setEmail("user" + userId + "@example.com");
user.setPhone("1380013800" + (userId % 100));
user.setAddress("北京市");
user.setGender(1);
user.setUsername("username" + userId);
return user;
}
}
四、测试验证:缓存功能完整性
编写测试用例,验证手动缓存和注解式缓存的核心功能:
java
package com.demo.redis;
import cn.hutool.json.JSONUtil;
import com.demo.redis.dto.User;
import com.demo.redis.service.IDemoService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest(classes = DemoRedisApplication.class)
public class DemoServiceTest {
@Autowired
private IDemoService iDemoService;
/**
* 测试手动缓存(RedisTemplate)
* 验证:缓存命中 → 过期 → 重新查库
*/
@Test
public void testGetUser() throws InterruptedException {
// 第一次:查库 + 存缓存
User user = iDemoService.getUser(100L);
log.info("第一次查询:{}", JSONUtil.toJsonStr(user));
// 第二次:缓存命中
user = iDemoService.getUser(100L);
log.info("第二次查询:{}", JSONUtil.toJsonStr(user));
// 等待30秒,缓存过期
Thread.sleep(30 * 1000);
// 第三次:重新查库 + 存缓存
user = iDemoService.getUser(100L);
log.info("第三次查询:{}", JSONUtil.toJsonStr(user));
}
/**
* 测试注解式缓存(@Cacheable/@CachePut/@CacheEvict)
*/
@Test
public void testUser() throws InterruptedException {
// 1. 第一次查:库 → 缓存
User user = iDemoService.getUserById(100L);
log.info("getUserById 1:{}", JSONUtil.toJsonStr(user));
// 2. 第二次查:缓存
user = iDemoService.getUserById(100L);
log.info("getUserById 2:{}", JSONUtil.toJsonStr(user));
// 3. 等待10秒,user缓存过期(配置的10秒)
Thread.sleep(10 * 1000);
// 4. 第三次查:库 → 缓存
user = iDemoService.getUserById(100L);
log.info("getUserById 3:{}", JSONUtil.toJsonStr(user));
// 5. 更新用户:同步缓存
user.setAddress("深圳市");
user = iDemoService.updateUser(user);
log.info("updateUser 1:{}", user.getAddress());
// 6. 验证更新后缓存
user = iDemoService.getUserById(100L);
log.info("getUserById 4:{}", user.getAddress());
// 7. 删除用户:清除缓存
iDemoService.deleteUser(100L);
log.info("deleteUser 1:完成");
// 8. 删除后查:库 → 缓存
user = iDemoService.getUserById(100L);
log.info("getUserById 5:{}", JSONUtil.toJsonStr(user));
}
/**
* 测试手动缓存与注解式缓存共存
*/
@Test
public void testGet() throws InterruptedException {
// 手动缓存存值
User user = iDemoService.getUser(100L);
log.info("getUser 1:{}", JSONUtil.toJsonStr(user));
// 注解式缓存读取(注意:key不同,手动是user:100,注解是demo:user:100)
user = iDemoService.getUserById(100L);
log.info("getUserById 2:{}", JSONUtil.toJsonStr(user));
}
}
测试结果分析
-
手动缓存:
- 第一次查询:打印
查询数据库:100+用户数据存入缓存:100; - 第二次查询:打印
从缓存获取用户:100; - 30 秒后第三次查询:重新打印
查询数据库:100。
- 第一次查询:打印
-
注解式缓存:
- 前两次
getUserById:仅第一次打印查询数据库:100; - 10 秒后第三次:重新打印
查询数据库:100(缓存过期); updateUser后:getUserById获取到更新后的address=深圳市;deleteUser后:getUserById重新打印查询数据库:100。
- 前两次
五、生产环境注意事项
1. 缓存问题防护
- 缓存穿透 :禁用
cacheNullValues(本文配置),结合参数校验 / 布隆过滤器; - 缓存击穿:热点 key 设置永不过期,或通过互斥锁(SETNX)控制缓存更新;
- 缓存雪崩 :给不同缓存名设置不同过期时间(如
user10 秒、order1 小时),避免集中过期; - 缓存一致性 :更新操作使用
@CachePut,删除操作使用@CacheEvict,保证缓存与数据库同步。
2. Redis 集群优化
- 配置
lettuce.cluster.refresh.adaptive=true,自适应刷新集群节点信息; - 合理设置连接池参数(
max-active/max-idle),避免连接数耗尽; - 生产环境建议配置 Redis 密码、超时时间,避免集群节点不可用导致服务阻塞。
3. 序列化注意事项
- 实体类建议实现
Serializable接口(兜底); - 时间类型必须自定义序列化规则,避免默认时间戳格式;
- 禁用
FAIL_ON_UNKNOWN_PROPERTIES,避免业务字段扩展导致反序列化失败。