Spring Boot 使用 Redis 的实战写法:配置、缓存、序列化与常见坑
- 发布时间:2026-06-16 10:06:50
- 本文热度:浏览 12 赞 0 评论 0
- 文章标签: Spring Boot Redis Spring Data Redis
- 全文共1字,阅读约需1分钟
在 Spring Boot 里用 Redis,先分清两种用法
Spring Boot 接 Redis 并不难,真正容易写乱的是使用方式:一类是把 Redis 当成数据结构工具,直接读写 String、Hash、List、Set、ZSet;另一类是把 Redis 当成缓存后端,交给 Spring Cache 的 @Cacheable、@CacheEvict 去管理。
这两种方式可以同时存在,但不要混着理解。RedisTemplate 关心的是 Redis 命令和序列化;@Cacheable 关心的是方法结果缓存、过期时间和失效策略。Spring Boot 官方文档也把 Redis 放在 NoSQL 支持里,说明 spring-boot-starter-data-redis 会收集相关依赖,默认使用 Lettuce,并提供 RedisConnectionFactory、StringRedisTemplate、RedisTemplate 等 Bean。([Home][1])
引入依赖
如果只是直接操作 Redis,引入这个 starter 就够了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
如果还要使用 @Cacheable 这套缓存注解,再加上缓存 starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
版本不要单独写死,交给 Spring Boot parent 或 BOM 管理。否则最常见的问题不是代码错,而是 Spring Data Redis、Lettuce、Jackson 之间版本不一致,启动时才爆一堆类找不到或方法签名不匹配。
配置连接信息
Spring Boot 3 之后常用的是 spring.data.redis.* 这组配置:
spring:
data:
redis:
host: localhost
port: 6379
database: 0
username:
password:
timeout: 3s
如果 Redis 有密码,填 password。如果使用 Redis URL,也可以这样写:
spring:
data:
redis:
url: redis://user:secret@localhost:6379
database: 0
官方文档里也明确说明,默认会尝试连接 localhost:6379,也可以用 spring.data.redis.* 或 spring.data.redis.url 指定连接信息。([Home][1])
直接操作 Redis:优先用 StringRedisTemplate 起步
最简单的读写可以从 StringRedisTemplate 开始。它的 key 和 value 都按字符串处理,调试时在 Redis CLI 里也能直接看懂。
@Service
public class RedisStringService {
private final StringRedisTemplate stringRedisTemplate;
public RedisStringService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void saveCode(String phone, String code) {
String key = "login:code:" + phone;
stringRedisTemplate.opsForValue().set(key, code, Duration.ofMinutes(5));
}
public boolean verifyCode(String phone, String inputCode) {
String key = "login:code:" + phone;
String code = stringRedisTemplate.opsForValue().get(key);
return inputCode != null && inputCode.equals(code);
}
}
opsForValue() 对应 Redis String,opsForHash() 对应 Hash,opsForList() 对应 List。Spring Data Redis 官方文档里也提到,RedisTemplate 是 Redis 模块的核心类,提供了较高层的 Redis 交互抽象,并封装了序列化和连接管理。([Home][2])
操作对象:不要放任默认序列化
很多项目一开始会直接注入 RedisTemplate<String, Object>,然后把 Java 对象塞进去:
redisTemplate.opsForValue().set("user:1", user);
能跑,但不一定适合长期维护。
RedisTemplate 默认序列化如果没有认真配置,很容易出现两类问题:第一,Redis 里看到的是一串不可读的二进制;第二,类名、包名变化后,历史缓存可能反序列化失败。Spring Data Redis 文档也说明,RedisTemplate 对多数操作默认使用 Java-based serializer,同时也允许替换序列化机制。([Home][2])
更常见的做法是:key 用字符串,value 用 JSON。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
然后再封装业务读写:
@Service
public class UserCacheService {
private final RedisTemplate<String, Object> redisTemplate;
public UserCacheService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void cacheUser(UserDTO user) {
String key = "user:" + user.getId();
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
}
public UserDTO getUser(Long userId) {
Object value = redisTemplate.opsForValue().get("user:" + userId);
if (value == null) {
return null;
}
return (UserDTO) value;
}
}
这里还有一个实际问题:GenericJackson2JsonRedisSerializer 会带类型信息,适合通用对象缓存;如果你对缓存结构有强约束,也可以用 Jackson2JsonRedisSerializer<UserDTO>,但配置会更啰嗦。没有绝对标准,关键是团队内部保持一致。
使用 Spring Cache:适合“方法结果缓存”
如果某个查询逻辑比较重,比如查数据库、组装 DTO、调用远程接口,可以用 Spring Cache 简化代码。
先开启缓存。不要随手把 @EnableCaching 扔到主启动类上,官方文档也提醒,放在主应用类上会让缓存成为测试套件里的强制特性。更稳妥的是单独建一个配置类。([Home][3])
@Configuration
@EnableCaching
public class CacheConfig {
}
配置 Redis 作为缓存类型:
spring:
cache:
type: redis
redis:
time-to-live: 30m
cache-null-values: false
业务代码:
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(cacheNames = "product", key = "#id")
public ProductDTO getProduct(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("product not found"));
return ProductDTO.from(product);
}
@CacheEvict(cacheNames = "product", key = "#id")
public void updateProduct(Long id, ProductUpdateRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("product not found"));
product.update(request);
productRepository.save(product);
}
}
这段代码的意思是:第一次调用 getProduct(1) 时走数据库,返回值写入 Redis;后续相同 key 直接从缓存取。更新商品后,用 @CacheEvict 删除对应缓存。
Spring Cache 的好处是业务代码干净;坏处是你对 Redis 细节的控制变少了。比如不同 cacheName 想配置不同过期时间,就需要自定义 RedisCacheManager。
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer())
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("product", defaultConfig.entryTtl(Duration.ofMinutes(10)));
cacheConfigs.put("user", defaultConfig.entryTtl(Duration.ofHours(1)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}
这个配置比注解多一点,但缓存策略终于落到了代码里,而不是散在各个方法上靠人记。
Key 设计比 API 更重要
很多 Redis 问题不是 RedisTemplate 用错,而是 key 设计太随意。
建议用固定前缀和业务维度:
login:code:13800000000
user:profile:10001
product:detail:9527
order:pay:lock:202605150001
不要这样写:
10001
user10001
product_9527
短期看没区别,线上排查时差很多。规范 key 至少有三个好处:
- 能快速判断数据属于哪个业务。
- 方便按前缀扫描和清理。
- 避免不同模块 key 冲突。
但也别在生产环境随便用 KEYS user:*。数据量大时它会阻塞 Redis。需要巡检或批量清理时,用 SCAN,或者从业务侧维护索引集合。
缓存穿透、击穿、雪崩要分开处理
这三个词经常被放在一起讲,但处理方式不一样。
缓存穿透:查询一个根本不存在的数据,比如用户 id 为 -1。请求每次都打到数据库。常见处理方式是缓存空值,或者做参数校验、布隆过滤器。
@Cacheable(cacheNames = "user", key = "#id", unless = "#result == null")
public UserDTO getUser(Long id) {
return userRepository.findById(id)
.map(UserDTO::from)
.orElse(null);
}
如果业务上允许缓存空值,就不要加 unless = "#result == null",而是给空值设置较短 TTL。
缓存击穿:某个热点 key 过期的一瞬间,大量请求同时打到数据库。常见处理方式是互斥锁、逻辑过期、热点 key 不轻易过期。
缓存雪崩:大量 key 在同一时间过期,数据库突然扛不住。处理方式是 TTL 加随机偏移,避免同批缓存同时失效。
public Duration randomTtl(int baseMinutes) {
int randomSeconds = ThreadLocalRandom.current().nextInt(0, 300);
return Duration.ofMinutes(baseMinutes).plusSeconds(randomSeconds);
}
缓存不是只要加上 Redis 就安全了。缓存真正麻烦的地方,是它让数据多了一份副本,而副本一定会带来一致性问题。
分布式锁:能不用就别滥用
Spring Boot 项目里经常会用 Redis 做简单分布式锁,比如防重复提交、定时任务抢占、订单支付状态更新。基本写法是 SET key value NX PX milliseconds,也就是“key 不存在才设置,并且带过期时间”。Redis 官方文档里 SET 命令支持 NX、EX、PX 等选项,其中 NX 表示 key 不存在才设置,PX 表示设置毫秒级过期时间。([Redis][4])
用 StringRedisTemplate 可以这样写:
@Service
public class RedisLockService {
private final StringRedisTemplate stringRedisTemplate;
public RedisLockService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean tryLock(String key, String value, Duration ttl) {
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, ttl);
return Boolean.TRUE.equals(success);
}
public void unlock(String key, String value) {
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";
stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
);
}
}
这里解锁不能直接 delete(key)。原因很简单:锁可能已经过期,又被别的线程拿到了。直接删除会把别人的锁删掉。所以加锁时要写入唯一 value,解锁时用 Lua 脚本判断 value 一致才删除。
如果业务对锁要求很高,比如锁续期、可重入、公平性、主从切换风险都要考虑,别自己硬写一套,直接用 Redisson 更现实。
常见坑
1. 忘记设置过期时间
登录验证码、短信频控、临时 token 这类数据必须设置 TTL。没有 TTL 的缓存,最后都会变成历史垃圾。
stringRedisTemplate.opsForValue()
.set("login:code:" + phone, code, Duration.ofMinutes(5));
2. 缓存和数据库更新顺序随便写
一般更新数据时,更常见的策略是:先更新数据库,再删除缓存。
@Transactional
public void updateUser(Long id, UserUpdateRequest request) {
userRepository.updateUser(id, request);
redisTemplate.delete("user:profile:" + id);
}
不要优先考虑“更新数据库后再更新缓存”。只要并发稍微复杂一点,缓存就可能被旧数据覆盖。删除缓存虽然看起来粗糙,但失败模式更简单。
3. 大 key
一个 Hash 里塞几十万个 field,一个 List 无限增长,一个 value 存几 MB JSON,都会拖慢 Redis。Redis 是单线程执行命令模型的典型代表,大 key 的问题通常不是“存不下”,而是删除、迁移、网络传输、慢查询都变重。
4. 把 Redis 当数据库
Redis 可以持久化,但它不是关系数据库的替代品。订单、账户余额、交易流水这类核心数据,主数据应该落在数据库里。Redis 更适合做缓存、计数器、排行榜、分布式协调、短期状态。
一个比较稳的落地方式
普通 Spring Boot 项目里,可以按这个顺序使用 Redis:
- 简单字符串场景,用
StringRedisTemplate。 - 对象缓存,明确配置 JSON 序列化。
- 方法级缓存,用 Spring Cache,但要统一 TTL 和 key 规则。
- 分布式锁只处理轻量并发控制,复杂锁交给成熟组件。
- 所有 Redis key 都要有命名规范,临时数据必须有过期时间。
这样用 Redis 不花哨,但稳定。项目里最怕的不是少用了几个高级命令,而是缓存逻辑散落在各处、key 没规范、序列化不统一、过期策略靠运气。前期省下的几分钟,后面基本都会在排查线上脏数据时还回去。
参考资料
- Spring Boot Reference Documentation:Working with NoSQL Technologies / Redis。([Home][1])
- Spring Boot Reference Documentation:Caching。([Home][3])
- Spring Data Redis Reference Documentation:Working with Objects through RedisTemplate。([Home][2])
- Redis 官方文档:SET command。([Redis][4])