整合 Redis
Java 项目整合 Redis,不只是“能 set/get 成功”。真正需要理解的是:客户端连接怎么管理、序列化怎么选、Key 怎么设计、缓存异常怎么处理。
为什么需要客户端封装
应用服务不能直接操作 Redis 协议细节,通常会使用客户端库:
flowchart TD
A["业务代码"] --> B["RedisTemplate / StringRedisTemplate"]
B --> C["RedisConnectionFactory"]
C --> D["Lettuce 或 Jedis"]
D --> E["Redis Server"]如果不通过成熟客户端,自己处理连接、重连、序列化、超时会非常容易出错。Spring Boot 默认通过 Spring Data Redis 封装常用操作。
Jedis 和 Lettuce
| 客户端 | 特点 | 注意点 |
|---|---|---|
| Jedis | API 简单,早期常用,连接不是线程安全 | 多线程要使用连接池 |
| Lettuce | 基于 Netty,连接可被多个线程共享 | Spring Boot 2.x 后默认使用 |
可以简单理解:
- Jedis 更像传统阻塞 IO,每个线程最好拿自己的连接。
- Lettuce 使用 Netty,适合异步和共享连接。
- Spring Boot 默认选择 Lettuce,普通项目优先使用默认方案即可。
Spring Boot 整合流程
flowchart TD
A["添加 starter"] --> B["配置 Redis 地址"]
B --> C["注入 StringRedisTemplate"]
C --> D["读写缓存"]
D --> E["设置 TTL"]
E --> F["处理缓存穿透和异常降级"]添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>配置连接
Spring Boot 2.x 常见配置:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=3s
spring.redis.lettuce.pool.max-active=16
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=2如果使用 Spring Boot 3.x,新版本配置前缀通常是:
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.timeout=3s具体以前项目的 Spring Boot 版本为准。
StringRedisTemplate Demo
如果只是存字符串或 JSON,优先使用 StringRedisTemplate,它的 key 和 value 默认都是字符串序列化,更直观。
@Service
public class ArticleCacheService {
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
public ArticleCacheService(StringRedisTemplate stringRedisTemplate,
ObjectMapper objectMapper) {
this.stringRedisTemplate = stringRedisTemplate;
this.objectMapper = objectMapper;
}
public void cacheArticle(Article article) throws JsonProcessingException {
String key = "article:detail:" + article.getId();
String value = objectMapper.writeValueAsString(article);
stringRedisTemplate.opsForValue().set(key, value, Duration.ofMinutes(30));
}
public Article getArticle(Long id) throws JsonProcessingException {
String key = "article:detail:" + id;
String value = stringRedisTemplate.opsForValue().get(key);
if (value == null) {
return null;
}
return objectMapper.readValue(value, Article.class);
}
}为什么推荐显式设置 TTL:缓存如果没有过期时间,旧数据可能长期存在,内存也可能不断增长。
RedisTemplate 序列化为什么重要
默认 RedisTemplate<Object,Object> 可能使用 JDK 序列化,Redis 中看到的 key/value 不直观,也不利于跨语言访问。
常见问题:
| 问题 | 后果 |
|---|---|
| Key 使用 JDK 序列化 | Redis CLI 看到乱码 key |
| Value 类型不统一 | 反序列化失败 |
| 使用过期的 default typing | 存在安全风险 |
| 多个服务序列化策略不同 | 同一个 key 互相读不出来 |
自定义配置示例:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}注意:如果项目已经大量使用某种序列化方式,不要直接切换,否则旧缓存可能无法读取。需要灰度或清理旧 key。
缓存查询 Demo
下面是典型旁路缓存写法。
public Article getArticle(Long id) throws JsonProcessingException {
String key = "article:detail:" + id;
String cache = stringRedisTemplate.opsForValue().get(key);
if (cache != null) {
if ("NULL".equals(cache)) {
return null;
}
return objectMapper.readValue(cache, Article.class);
}
Article article = articleMapper.selectById(id);
if (article == null) {
stringRedisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));
return null;
}
stringRedisTemplate.opsForValue().set(
key,
objectMapper.writeValueAsString(article),
Duration.ofMinutes(30)
);
return article;
}这个 Demo 解决两个问题:
- 正常数据命中缓存,减少数据库压力。
- 不存在的数据缓存空值,减少缓存穿透。
更新数据时怎么处理缓存
常见策略是“先更新数据库,再删除缓存”。
flowchart TD
A["修改文章"] --> B["更新 MySQL"]
B --> C["删除 Redis 缓存"]
C --> D["下一次读取未命中"]
D --> E["查 MySQL 并重建缓存"]示例:
@Transactional
public void updateArticle(Article article) {
articleMapper.updateById(article);
stringRedisTemplate.delete("article:detail:" + article.getId());
}为什么不是先删缓存再更新数据库:先删缓存后,如果数据库还没更新完成,另一个请求可能立刻查库并把旧值写回缓存。
严格一致性很难只靠缓存保证。多数业务采用短 TTL、删除缓存、异步补偿等方式达到最终一致。
常见坑
| 问题 | 后果 | 建议 |
|---|---|---|
| 不设置超时 | Redis 卡住拖垮应用线程 | 配置连接和命令超时 |
| 不设置 TTL | 内存持续增长,旧数据不失效 | 缓存必须有过期策略 |
| Key 没有前缀 | 难排查,容易冲突 | 使用 业务:模块:id |
| 直接缓存大对象 | 网络慢、Redis 内存压力大 | 缓存必要字段,拆分大 Key |
| Redis 异常直接报错 | 缓存故障影响主流程 | 核心查询要考虑降级 |
| 锁没有 token | 可能误删别人的锁 | 使用唯一 token + Lua 删除 |
小结
Spring Boot 整合 Redis 要掌握四件事:
- 连接:优先使用 Spring Boot 默认 Lettuce。
- 序列化:key 用字符串,value 明确 JSON 或统一序列化。
- 缓存:读缓存、未命中查库、写缓存、设置 TTL。
- 治理:处理穿透、击穿、一致性、超时和降级。
能 set/get 只是第一步,真正上线要考虑序列化兼容、缓存一致性和 Redis 异常时系统是否还能保护数据库。
