Spring Boot 使用 Redis:从接入配置到缓存实践
- 发布时间:2026-04-26 06:02:12
- 本文热度:浏览 5 赞 0 评论 0
- 文章标签: Spring Boot Redis 缓存
- 全文共1字,阅读约需1分钟
Spring Boot 使用 Redis:从接入到落地的完整实践
Redis 在 Spring Boot 项目里最常见的用途,不是“为了用缓存而用缓存”,而是解决几个非常具体的问题:降低热点数据的数据库压力、缩短接口响应时间、实现分布式共享状态,以及支撑限流、分布式锁、排行榜、会话管理等场景。你给出的写作约束和主题我已按要求执行。
这篇文章直接从实际开发出发,讲清楚 Spring Boot 如何接入 Redis、底层用的是什么、应该怎么配置、序列化怎么选、常见业务场景怎么写,以及上线前要注意哪些坑。
为什么 Spring Boot 项目里常用 Redis
Redis 是基于内存的 Key-Value 数据库,读写延迟低,支持丰富的数据结构。对 Spring Boot 应用来说,它最典型的价值有四类:
1. 做缓存
把高频读取、低频变更的数据放进 Redis,减少数据库查询次数。比如商品详情、用户基础信息、字典配置、首页聚合数据。
2. 做共享状态存储
多节点部署后,单机内存里的状态无法共享,这时可以把验证码、登录态、接口幂等标记等放到 Redis。
3. 做分布式协调
例如分布式锁、延时任务、库存扣减保护、限流计数器等,都可以基于 Redis 快速实现。
4. 做高性能数据结构支撑
排行榜可以用 ZSet,消息消费状态可以用 Stream,去重标记可以用 Set,访问频次统计可以用 Hash 或 String 计数器。
Spring Boot 整合 Redis 用的是什么
Spring Boot 整合 Redis,核心并不是自己去直接操作 Redis 协议,而是通过 Spring Data Redis 统一封装。
常见技术栈关系如下:
- Spring Boot:负责自动配置
- Spring Data Redis:提供 RedisTemplate、StringRedisTemplate、缓存抽象等能力
- Lettuce:默认 Redis 客户端
- Redis Server:真正存储数据的服务端
Jedis 和 Lettuce 的区别
Spring Boot 2.x 以后默认使用 Lettuce,原因很直接:
- Lettuce 基于 Netty,线程模型更现代
- 支持同步、异步、响应式
- 连接复用能力更好
- 更适合高并发场景
如果不是历史项目兼容,一般不建议新项目再选 Jedis。
先搭一个最小可用环境
下面以 Spring Boot 3.x 为例。
1. 引入依赖
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存抽象 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- JSON 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2. 配置连接信息
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 3000ms
3. 启用缓存能力
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableCaching
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
到这一步,Spring Boot 已经能连上 Redis,但离“适合生产”的状态还差一步:序列化配置。
为什么 Redis 序列化配置必须自己管
很多项目 Redis 出问题,不是因为连接失败,而是因为:
- key 可读性差
- value 存进去是乱码样式
- 不同服务反序列化失败
- 类结构一改,历史缓存读不出来
- 直接用了 JDK 序列化,数据臃肿、可维护性差
默认情况下,如果直接用 RedisTemplate<Object, Object>,很容易踩到序列化坑。实际项目里通常建议:
- Key 使用 String 序列化
- Value 使用 JSON 序列化
- Hash 的 key 和 value 也保持一致策略
推荐的 RedisTemplate 配置
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
为什么不直接用默认配置
因为默认配置对开发不友好,尤其在排查问题时很痛苦。你在 Redis 客户端里看到清晰的 key 和 JSON,可维护性会高很多。
StringRedisTemplate 和 RedisTemplate 怎么选
这两个类很多人会混。
StringRedisTemplate
它的 key 和 value 都按字符串处理,适合:
- 计数器
- 简单缓存
- 验证码
- 分布式锁标记
- 限流 key
示例:
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CodeService {
private final StringRedisTemplate stringRedisTemplate;
public CodeService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void saveCode(String mobile, String code) {
String key = "sms:code:" + mobile;
stringRedisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
}
public String getCode(String mobile) {
return stringRedisTemplate.opsForValue().get("sms:code:" + mobile);
}
}
RedisTemplate<String, Object>
适合存对象、集合结构、业务缓存实体等。
示例:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserCacheService {
private final RedisTemplate<String, Object> redisTemplate;
public UserCacheService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void saveUser(Long userId, UserCacheDTO user) {
String key = "user:info:" + userId;
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
public UserCacheDTO getUser(Long userId) {
Object value = redisTemplate.opsForValue().get("user:info:" + userId);
return value == null ? null : (UserCacheDTO) value;
}
}
缓存场景里更推荐用 Spring Cache
如果你的目标是“给查询接口加缓存”,优先考虑 Spring Cache,而不是手写 RedisTemplate。
原因很简单:
- 代码更少
- 语义更清晰
- 支持统一过期策略
- 支持注解式缓存清理
- 更适合标准 CRUD 场景
配置 RedisCacheManager
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
注解式缓存的常见写法
查询时缓存:@Cacheable
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Cacheable(value = "product", key = "#id")
public ProductDTO getById(Long id) {
System.out.println("查询数据库...");
return loadFromDb(id);
}
private ProductDTO loadFromDb(Long id) {
ProductDTO dto = new ProductDTO();
dto.setId(id);
dto.setName("机械键盘");
dto.setPrice(399.00);
return dto;
}
}
第一次查数据库,后面同样的 key 直接走 Redis。
更新时刷新缓存:@CachePut
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class ProductUpdateService {
@CachePut(value = "product", key = "#product.id")
public ProductDTO update(ProductDTO product) {
// 先更新数据库
// 再把新值写入缓存
return product;
}
}
删除时清理缓存:@CacheEvict
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class ProductDeleteService {
@CacheEvict(value = "product", key = "#id")
public void deleteById(Long id) {
// 删除数据库记录
}
}
一个典型的缓存读写流程
以商品详情为例,推荐流程如下:
- 先查 Redis
- 没有命中,再查数据库
- 查到结果后写 Redis,并设置过期时间
- 数据更新时,删除或刷新缓存
这个模式本质上就是 Cache Aside Pattern,也是业务系统里最常见的缓存模式。
代码示例:手写缓存逻辑
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final ProductRepository productRepository;
public ProductCacheService(RedisTemplate<String, Object> redisTemplate,
ProductRepository productRepository) {
this.redisTemplate = redisTemplate;
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
String key = "product:" + id;
Object cacheObj = redisTemplate.opsForValue().get(key);
if (cacheObj != null) {
return (Product) cacheObj;
}
Product product = productRepository.findById(id);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
}
return product;
}
public void updateProduct(Product product) {
productRepository.update(product);
redisTemplate.delete("product:" + product.getId());
}
}
数据库示例:商品表完整建表 SQL
既然示例里涉及数据库,这里给出一份完整建表 SQL。
CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` VARCHAR(100) NOT NULL COMMENT '商品名称',
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '商品价格',
`stock` INT NOT NULL DEFAULT 0 COMMENT '库存',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 1上架 0下架',
`description` TEXT COMMENT '商品描述',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';
Redis 常见数据类型在 Spring Boot 里的用法
Redis 不是只能存字符串。不同结构适合不同业务语义。
1. String:最常用
适合缓存对象 JSON、计数器、验证码、token。
stringRedisTemplate.opsForValue().set("login:token:1001", "abc123", 30, TimeUnit.MINUTES);
2. Hash:适合结构化对象字段
适合存用户属性、配置项。
redisTemplate.opsForHash().put("user:1001", "nickname", "张三");
redisTemplate.opsForHash().put("user:1001", "age", 28);
3. List:适合顺序列表
适合消息队列的简单实现、最近访问记录。
redisTemplate.opsForList().leftPush("news:list", "article-10001");
4. Set:适合去重
适合标签、点赞用户集合、共同关注判断。
redisTemplate.opsForSet().add("post:like:1001", "u1", "u2", "u3");
5. ZSet:适合排序
适合排行榜、积分榜、热度排序。
redisTemplate.opsForZSet().add("rank:score", "tom", 98);
redisTemplate.opsForZSet().add("rank:score", "jack", 100);
分布式锁可以怎么做
Redis 经常被拿来实现分布式锁,但实现得不严谨,问题会很多。
最基础的写法是使用 setIfAbsent,也就是 SETNX。
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLock {
private final StringRedisTemplate stringRedisTemplate;
public RedisLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public String tryLock(String key, long timeoutSeconds) {
String value = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, timeoutSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success) ? value : null;
}
public void unlock(String key, String value) {
String currentValue = stringRedisTemplate.opsForValue().get(key);
if (value != null && value.equals(currentValue)) {
stringRedisTemplate.delete(key);
}
}
}
这段代码有什么问题
上面的 unlock 不是原子操作:先 get,再 delete,中间可能发生线程切换,导致误删别人的锁。
更稳妥的方式是使用 Lua 脚本,把“判断 value 是否一致”和“删除 key”做成一个原子操作。
更推荐的方案
生产环境优先考虑:
- Redisson
- Lua 脚本保证原子性
- 明确锁过期时间
- 明确业务是否允许锁续期
- 明确加锁失败后的补偿策略
Redis 能做分布式锁,但不要把“能跑”误认为“能稳定上线”。
缓存穿透、击穿、雪崩怎么处理
这三个词被提得很多,但项目里真正要会的是对应处理方式。
1. 缓存穿透
请求的数据本来就不存在,Redis 没有,数据库也没有,请求每次都打到数据库。
处理办法
- 缓存空值,设置较短 TTL
- 参数校验
- 使用布隆过滤器做前置拦截
2. 缓存击穿
某个热点 key 失效瞬间,大量请求同时打到数据库。
处理办法
- 热点 key 永不过期,后台异步刷新
- 互斥锁 / 单飞机制
- 逻辑过期而不是物理同时过期
3. 缓存雪崩
大量 key 在同一时间集中失效,请求涌向数据库。
处理办法
- TTL 加随机值
- 多级缓存
- 限流降级
- Redis 高可用部署
过期时间不要整齐划一
这是很常见但非常容易被忽略的问题。
错误示例:
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
如果系统里很多 key 都在同一时刻批量写入,30 分钟后也可能在同一时刻大面积失效。
更好的做法是增加随机过期时间:
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
long baseTtl = 30;
long randomTtl = ThreadLocalRandom.current().nextLong(1, 6);
redisTemplate.opsForValue().set(key, value, baseTtl + randomTtl, TimeUnit.MINUTES);
缓存更新时,删除还是覆盖
这是 Redis 落地里很实际的问题。
通常更推荐:先更新数据库,再删除缓存。
原因是:
- 删除缓存实现简单
- 兼容大部分业务场景
- 避免缓存和数据库双写时的更多一致性问题
推荐流程:
- 更新数据库
- 删除 Redis 缓存
- 后续读取时重新加载
而“先删缓存再更数据库”容易让并发读请求把旧值重新写回缓存。
Key 设计要有规范
Redis 一旦在多个模块里广泛使用,最先乱掉的通常不是性能,而是 key 命名。
推荐规范:
业务:模块:实体:标识
例如:
user:profile:1001
order:detail:202604140001
product:stock:20086
sms:code:13800138000
lock:order:create:1001
这样设计的好处
- 可读性高
- 方便按业务排查
- 方便统一失效策略
- 方便做前缀统计和监控
不建议把 Redis 当数据库硬存
Redis 可以持久化,但它首先是高性能内存数据库,不是用来替代 MySQL 的业务主存储。
不建议把 Redis 直接作为唯一数据源去承担:
- 核心交易数据
- 强一致订单数据
- 复杂关系查询
- 长期归档型数据
Redis 更适合:
- 临时态
- 派生数据
- 热点数据
- 快速查询副本
- 协调状态
Spring Boot 使用 Redis 时的生产建议
1. 统一封装缓存层
不要在 Controller、Service、Job 里到处直接拼 key。建议封装统一缓存服务或 key 生成器。
2. 明确缓存边界
不是所有数据都适合缓存。高频访问、变化不频繁、可接受短暂不一致的数据最适合。
3. 监控命中率
Redis 用了不代表一定有效。没有命中率监控,就不知道它到底是在减压还是在浪费内存。
4. 做好大 key 管控
大对象、大列表、大 Hash 都会影响 Redis 性能,也会影响网络传输和序列化开销。
5. 防止缓存 null 污染
缓存空值是必要手段,但要加短 TTL,避免长期污染缓存空间。
6. 区分业务库编号
Redis 支持多个 logical database,但在生产里更常见的是通过前缀隔离,而不是强依赖不同 db 编号做业务隔离。
7. 为热点接口设计退路
Redis 宕机时,如果所有流量直接冲数据库,数据库可能也扛不住。要考虑限流、降级和兜底默认值。
Spring Boot 2.x 和 3.x 使用 Redis 的区别
这里明确标注版本差异。
Spring Boot 2.x
常见配置前缀通常写为:
spring:
redis:
host: 127.0.0.1
port: 6379
Spring Boot 3.x
Redis 配置更常见的是:
spring:
data:
redis:
host: 127.0.0.1
port: 6379
实际开发怎么处理
看你项目使用的 Spring Boot 版本。如果是升级项目,配置前缀不一致会直接导致连接配置失效,这是迁移时必须检查的一项。
一个更实际的落地建议
在 Spring Boot 项目里用 Redis,不要一上来就把所有业务都塞进去。更合理的顺序是:
- 先用于热点查询缓存
- 再用于验证码、登录态、限流计数器
- 再逐步引入排行榜、分布式锁等高级场景
- 最后再考虑复杂结构和高可用架构
因为 Redis 本身不复杂,复杂的是并发、一致性、失效策略、监控和故障处理。
总结
Spring Boot 使用 Redis,本质上不是“连上就行”,而是围绕几个关键问题做工程化落地:
- 连接和客户端配置要明确
- 序列化策略要统一
- 缓存模式要清楚
- key 设计要规范
- 过期时间要合理
- 并发场景要防穿透、防击穿、防雪崩
- 分布式锁这类高级用法不能只停留在能跑
真正写进生产环境的 Redis 代码,重点从来不是 API 会不会调,而是你是否清楚数据为什么放进去、什么时候失效、失效后系统会发生什么,以及出故障时有没有退路。