Appearance
第60课:Spring Data Redis
🎯 学习目标
- 理解 Redis 在 Java 后端中的常见用途:缓存、计数、排行榜、分布式锁、限流。
- 掌握
StringRedisTemplate、RedisTemplate、序列化配置和常见数据结构操作。 - 能正确使用 Spring Cache 注解,并理解它和手写 Redis 操作的边界。
- 能识别缓存穿透、击穿、雪崩、大 Key、热 Key 和锁误删等问题。
- 能设计可维护的 Key 命名、TTL、序列化和监控策略。
📖 一、Redis 适合做什么
Redis 是内存型数据存储,常见用途:
text
缓存热点数据
分布式锁
计数器
限流
排行榜
会话存储
延迟队列的辅助结构
布隆过滤器不适合:
text
替代关系型数据库做强一致主存储
存巨大对象
无限增长的无 TTL 缓存
高复杂关系查询Redis 很快,但引入后会增加一致性和运维复杂度。
📖 二、依赖和配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>配置:
yaml
spring:
data:
redis:
host: localhost
port: 6379
timeout: 3s
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2Spring Boot 3 使用 spring.data.redis 前缀,老版本常见 spring.redis。
📖 三、Template 选择
StringRedisTemplate:
java
@Service
public class TokenService {
private final StringRedisTemplate redisTemplate;
public void saveToken(String token, String userId) {
redisTemplate.opsForValue().set("token:" + token, userId, Duration.ofHours(2));
}
}适合 key/value 都是字符串的场景。
RedisTemplate<String, Object>:
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}序列化必须统一。不要让 key 变成不可读的二进制。
📖 四、常见数据结构
1. String
java
redisTemplate.opsForValue().set("user:1001:name", "Alice", Duration.ofMinutes(10));
String name = redisTemplate.opsForValue().get("user:1001:name");适合简单值、JSON 对象、计数器。
2. Hash
java
redisTemplate.opsForHash().put("user:1001", "name", "Alice");
redisTemplate.opsForHash().put("user:1001", "email", "alice@example.com");
Object name = redisTemplate.opsForHash().get("user:1001", "name");适合对象局部字段更新。
3. List
java
redisTemplate.opsForList().leftPush("queue:email", "task-1");
Object task = redisTemplate.opsForList().rightPop("queue:email");简单队列可以用 List,但可靠消息队列应使用专业 MQ。
4. Set
java
redisTemplate.opsForSet().add("user:1001:roles", "ADMIN", "USER");
Boolean isAdmin = redisTemplate.opsForSet().isMember("user:1001:roles", "ADMIN");5. ZSet
java
redisTemplate.opsForZSet().add("rank:score", "user:1001", 98.5);
Set<Object> top = redisTemplate.opsForZSet().reverseRange("rank:score", 0, 9);适合排行榜。
📖 五、Spring Cache 注解
启用缓存:
java
@EnableCaching
@Configuration
public class CacheConfig {
}使用:
java
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
@Cacheable(key = "#id")
public UserResponse getById(Long id) {
return userRepository.findResponseById(id);
}
@CacheEvict(key = "#id")
public void delete(Long id) {
userRepository.deleteById(id);
}
}@Cacheable 适合简单缓存;复杂一致性、逻辑过期、热点保护等场景建议手写缓存逻辑。
📖 六、分布式锁
基础写法:
java
public boolean tryLock(String key, String value, Duration ttl) {
Boolean ok = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, ttl);
return Boolean.TRUE.equals(ok);
}释放锁必须校验 value,避免误删别人的锁:
java
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
""";生产项目可以优先考虑 Redisson,它处理了可重入、看门狗、锁续期等细节。
⚠️ 七、常见陷阱
1. 没有 TTL
缓存不设置过期时间,最终可能撑爆内存。
2. 序列化不统一
不同服务使用不同序列化方式,会导致读取失败或数据不可读。
3. 大 Key
单个 value 很大或 hash/list/zset 成员过多,会阻塞 Redis。
4. 热 Key
少数 Key 被极高并发访问,可能打满单分片。可以用本地缓存、Key 分片或热点保护。
5. 分布式锁没有唯一 value
释放锁时不校验 value,可能删除其他线程刚获得的锁。
🆚 八、Java vs C 对比
| 维度 | C Redis 客户端 | Spring Data Redis |
|---|---|---|
| 命令调用 | 手写命令或 hiredis | Template 封装 |
| 序列化 | 手动处理字符串/二进制 | Serializer 配置 |
| 缓存注解 | 通常没有 | Spring Cache |
| 连接池 | 手动配置 | Lettuce/Jedis 集成 |
Spring Data Redis 提高了集成效率,但 Redis 的数据结构、过期策略和一致性问题仍需自己设计。
💡 九、最佳实践
- Key 使用统一命名规范,例如
业务:对象:ID。 - 所有缓存都应有 TTL 或明确容量控制。
- 使用字符串 key,便于排查。
- 不要缓存巨大对象,必要时拆分字段。
- 更新数据库后删除缓存,并设计失败补偿。
- 分布式锁 value 必须唯一,释放必须原子校验。
- 高可靠锁优先使用 Redisson。
- 监控命中率、延迟、内存、大 Key、热 Key、连接池。
🧭 十、项目落地清单
text
Redis 连接池是否配置?
key/value 序列化是否统一?
缓存 key 是否有命名规范?
TTL 是否合理?
是否有大 Key 和热 Key 监控?
缓存更新策略是否明确?
缓存失败是否能降级?
分布式锁是否防误删?🔍 十一、自测问题
text
StringRedisTemplate 和 RedisTemplate 有什么区别?
为什么 key 序列化建议用 StringRedisSerializer?
@Cacheable 适合什么场景?
为什么缓存必须设置 TTL?
大 Key 和热 Key 分别有什么风险?
分布式锁为什么需要唯一 value?
Redisson 比手写 setIfAbsent 多解决了哪些问题?
Redis 为什么不能随便替代数据库?🧭 十二、项目落地清单
接入 Redis 前,至少确认:
text
Redis 是缓存还是主存储?
Key 命名是否统一?
Value 序列化是否统一?
TTL 是否设置?
是否有缓存穿透保护?
热点 Key 是否有保护策略?
更新数据库后如何失效缓存?
删除缓存失败如何补偿?
Redis 宕机时系统如何降级?
是否监控连接池、延迟、内存、大 Key?如果 Redis 只是为了“看起来架构完整”,但没有明确命中率和性能收益,就不要急着引入。
🧪 十三、实战案例:缓存商品详情
java
public ProductResponse getProduct(Long productId) {
String key = "product:detail:" + productId;
ProductResponse cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
ProductEntity entity = productRepository.findById(productId)
.orElseThrow(() -> new NotFoundException("商品不存在"));
ProductResponse response = ProductResponse.from(entity);
long ttl = 3600 + ThreadLocalRandom.current().nextLong(300);
redisTemplate.opsForValue().set(key, response, Duration.ofSeconds(ttl));
return response;
}这里有几个设计点:
text
Key 带业务域和 ID。
TTL 加随机抖动,降低雪崩风险。
只缓存 Response DTO,不缓存 JPA Entity。
不存在数据要考虑空值缓存或布隆过滤器。📌 十四、学习建议
建议自己模拟:
text
同一商品高并发查询
商品不存在导致穿透
缓存过期瞬间大量请求
更新商品后缓存未删除Redis 学习重点不是命令数量,而是失败模式。
📚 十五、Redis 数据结构选择速查
| 需求 | 推荐结构 |
|---|---|
| 缓存对象 | String 或 Hash |
| 计数器 | String + INCR |
| 去重 | Set |
| 排行榜 | ZSet |
| 简单队列 | List |
| 用户在线状态 | String/Set |
| 频率限制 | String + TTL 或 Lua |
选择数据结构时要同时考虑:
text
读写复杂度
Key 数量
Value 大小
是否需要局部更新
是否需要排序
是否需要过期📌 十六、生产排查重点
Redis 慢时优先看:
text
是否有大 Key。
是否有热 Key。
网络延迟是否升高。
连接池是否耗尽。
是否执行了危险命令。
内存是否接近上限。
淘汰策略是否触发。🎓 小结
Spring Data Redis 让 Java 项目接入 Redis 变得简单,但真正的难点在缓存策略、序列化、一致性和故障保护。会调用 Redis API 只是开始,能设计可控的缓存系统才是重点。