Skip to content

第60课:Spring Data Redis

🎯 学习目标

  • 理解 Redis 在 Java 后端中的常见用途:缓存、计数、排行榜、分布式锁、限流。
  • 掌握 StringRedisTemplateRedisTemplate、序列化配置和常见数据结构操作。
  • 能正确使用 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: 2

Spring 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&lt;String, Object&gt;

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
命令调用手写命令或 hiredisTemplate 封装
序列化手动处理字符串/二进制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 只是开始,能设计可控的缓存系统才是重点。