Chapter 06

Spring Cache 与 Redis

缓存抽象设计,注解式缓存,RedisTemplate 直接操作,分布式 Session 与缓存一致性策略。

为什么需要缓存

数据库是大多数 Web 应用的性能瓶颈。即便是高性能的 PostgreSQL,单实例 QPS 也通常在数千到数万之间,而 Redis 单机 QPS 可达 10 万+。合理使用缓存可以带来:

核心概念名词解释

@Cacheable
标注在方法上,第一次调用时执行方法并将结果存入缓存;后续相同参数的调用直接返回缓存值,不执行方法体。适用于读多写少的查询方法。
@CacheEvict
在方法执行后(或前)删除指定的缓存条目。通常在数据更新或删除操作时使用,确保缓存与数据库保持同步。allEntries=true 可清空整个缓存命名空间。
@CachePut
每次执行方法并将结果更新到缓存,不会跳过方法执行。适用于更新操作——即更新数据库又更新缓存,保持一致性。
CacheManager
缓存管理器,是 Spring Cache 抽象的核心接口。通过配置不同的 CacheManager 实现(RedisCacheManager、CaffeineCacheManager),可以无缝切换缓存后端,业务代码无需改变。
RedisTemplate<K, V>
Spring Data Redis 提供的 Redis 操作模板,支持所有 Redis 数据结构(String/Hash/List/Set/ZSet)。StringRedisTemplate 是其预配置子类,key 和 value 均使用 String 序列化。
TTL(Time To Live)
缓存条目的生存时间,过期后自动删除。设置合理的 TTL 是缓存策略的关键——TTL 太短缓存效果差,TTL 太长数据陈旧风险高。不同业务场景应设置不同 TTL。

缓存命中与未命中流程

客户端请求 GET /users/123 │ ▼ @Cacheable("users", key="#id") │ ├── 查找缓存键 "users::123" │ ├── 【缓存命中 Cache Hit】 │ │ │ └── 直接返回缓存值(不执行方法体) │ 耗时:<1ms │ └── 【缓存未命中 Cache Miss】 │ ▼ 执行方法体 │ ▼ 查询数据库 SELECT * FROM users WHERE id=123 │ 耗时:5-50ms ▼ 将结果存入 Redis(设置 TTL=30min) │ └── 返回结果给客户端 【数据更新时】 @CacheEvict("users", key="#id") ← 更新/删除数据后清除缓存 下次请求将触发 Cache Miss → 重新加载最新数据

配置 Redis 缓存

@Configuration
@EnableCaching  // 开启 Spring Cache 注解支持
public class CacheConfig {

    @Bean
    public RedisCacheConfiguration defaultCacheConfig() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))   // 默认 30 分钟 TTL
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .disableCachingNullValues();  // null 不缓存(防止缓存穿透)
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        // 为不同缓存设置不同 TTL
        Map<String, RedisCacheConfiguration> configs = new HashMap<>();
        configs.put("users", defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));
        configs.put("products", defaultCacheConfig().entryTtl(Duration.ofHours(2)));

        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaultCacheConfig())
            .withInitialCacheConfigurations(configs)
            .build();
    }
}

注解式缓存使用

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    // 查询时缓存(key = "users::123")
    @Cacheable(value = "users", key = "#id")
    @Transactional(readOnly = true)
    public UserResponseDTO findById(Long id) {
        // 只有缓存未命中时才执行此方法
        return userRepository.findById(id)
            .map(this::toDTO)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在"));
    }

    // 更新后刷新缓存(执行方法并更新缓存)
    @CachePut(value = "users", key = "#result.id")
    @Transactional
    public UserResponseDTO update(Long id, UserUpdateDTO dto) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在"));
        user.setUsername(dto.getUsername());
        return toDTO(userRepository.save(user));
    }

    // 删除时清除缓存
    @CacheEvict(value = "users", key = "#id")
    @Transactional
    public void delete(Long id) {
        userRepository.deleteById(id);
    }
}

RedisTemplate 直接操作

当需要更精细的 Redis 控制时(计数器、排行榜、分布式锁),直接使用 RedisTemplate:

@Service
@RequiredArgsConstructor
public class RedisService {

    private final StringRedisTemplate redisTemplate;

    // String 操作:计数器
    public void incrementLoginCount(Long userId) {
        String key = "login:count:" + userId;
        redisTemplate.opsForValue().increment(key);
        redisTemplate.expire(key, Duration.ofDays(7));
    }

    // Hash 操作:存储用户会话信息
    public void saveUserSession(String sessionId, Map<String, String> userInfo) {
        String key = "session:" + sessionId;
        redisTemplate.opsForHash().putAll(key, userInfo);
        redisTemplate.expire(key, Duration.ofHours(2));
    }

    // ZSet 操作:商品热度排行榜
    public void recordProductView(Long productId) {
        redisTemplate.opsForZSet().incrementScore(
            "product:hot", productId.toString(), 1.0);
    }

    // 获取 Top 10 热门商品
    public Set<String> getTop10Products() {
        return redisTemplate.opsForZSet().reverseRange("product:hot", 0, 9);
    }

    // 分布式锁(简单实现)
    public boolean tryLock(String lockKey, String lockValue, long seconds) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(seconds));
        return Boolean.TRUE.equals(result);
    }
}

分布式 Session

<!-- Spring Session + Redis -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
  session:
    store-type: redis      # Session 存储到 Redis
    timeout: 2h            # Session 过期时间
  data:
    redis:
      host: localhost
      port: 6379
      password: ${REDIS_PASSWORD}
Warning 缓存与数据库的一致性是经典难题。最常见的可靠策略是"先写数据库,再删缓存"(Cache Aside Pattern):更新时先更新 DB,再删除缓存(而不是更新缓存)。下次读请求时缓存未命中,重新从 DB 加载最新数据填充缓存。避免使用"先删缓存,再写数据库",因为并发场景下极易读到旧数据。
Danger 注意缓存雪崩(大量缓存同时过期)、缓存穿透(查询不存在的数据)、缓存击穿(热点 key 过期瞬间大量请求打穿到DB)三大经典问题。分别用 TTL 加随机抖动、布隆过滤器/空值缓存、分布式锁或不过期策略来应对。