RedisNotes

第 08 章:Spring Boot 集成

zjc 于 2026-01-08 发布

这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Spring Boot 让 Redis 接入非常容易,但默认配置往往不适合生产。本章覆盖 RedisTemplate、序列化、缓存注解、连接配置、异常处理与常见坑。

8.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

默认客户端是 Lettuce。需要连接池时必须引入 commons-pool2

8.2 基础配置

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: redis123
      database: 0
      timeout: 1s
      connect-timeout: 500ms
      client-name: order-service
      lettuce:
        pool:
          enabled: true
          max-active: 100
          max-idle: 50
          min-idle: 10
          max-wait: 500ms

Spring Boot 3 使用 spring.data.redis.*;2.x 是 spring.redis.*

8.3 RedisTemplate

最基础用法:

@Service
@RequiredArgsConstructor
public class ProductService {
    private final RedisTemplate<String, Object> redisTemplate;

    public Product getProduct(String id) {
        String key = "shop:product:" + id;
        return (Product) redisTemplate.opsForValue().get(key);
    }

    public void save(Product product) {
        redisTemplate.opsForValue()
                .set("shop:product:" + product.getId(),
                     product, Duration.ofMinutes(10));
    }
}

不同数据结构:

redisTemplate.opsForValue();     // String
redisTemplate.opsForHash();      // Hash
redisTemplate.opsForList();      // List
redisTemplate.opsForSet();       // Set
redisTemplate.opsForZSet();      // ZSet
redisTemplate.opsForStream();    // Stream

8.4 序列化配置

默认 JdkSerializationRedisSerializer 会把 key 变成乱码字节,且存在兼容与体积问题。推荐 key 用 String,value 用 JSON:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer keySerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer valueSerializer =
                new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(keySerializer);
        template.setHashKeySerializer(keySerializer);
        template.setValueSerializer(valueSerializer);
        template.setHashValueSerializer(valueSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

GenericJackson2JsonRedisSerializer 会写入 @class 类型信息,便于反序列化为原类型。跨语言系统可去掉类型信息,改用显式 DTO 反序列化。

8.5 StringRedisTemplate

@Service
@RequiredArgsConstructor
public class CounterService {
    private final StringRedisTemplate redis;

    public long increase(String key) {
        return redis.opsForValue().increment(key);
    }
}

适合:

StringRedisTemplate 的 key 和 value 都是字符串,最不容易出现序列化黑盒。

8.6 缓存注解

启用:

@EnableCaching
@SpringBootApplication
public class Application { }

使用:

@Service
public class ProductQueryService {

    @Cacheable(value = "product", key = "#id", unless = "#result == null")
    public Product findProduct(String id) {
        return productMapper.findById(id);
    }

    @CachePut(value = "product", key = "#product.id")
    public Product update(Product product) {
        productMapper.update(product);
        return product;
    }

    @CacheEvict(value = "product", key = "#id")
    public void delete(String id) {
        productMapper.delete(id);
    }
}

注解语义:

注解 行为
@Cacheable 先查缓存,命中则不执行方法
@CachePut 执行方法并更新缓存
@CacheEvict 删除缓存
@Caching 组合多个缓存操作
@CacheConfig 类级别公共配置

TTL 配置

@Configuration
public class CacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration
                .defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String, RedisCacheConfiguration> configs = Map.of(
                "product", config.entryTtl(Duration.ofMinutes(5)),
                "rank", config.entryTtl(Duration.ofMinutes(1))
        );

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

8.7 缓存注解的坑

1. 同类内部调用失效

public Product query(String id) {
    return this.findProduct(id); // AOP 代理不生效
}

解法:拆到另一个 Bean,或注入自身代理。

2. key 表达式写错

@Cacheable(value = "product", key = "#id")

参数名编译后可能不可用,建议加 -parameters 或显式写参数位置:

@Cacheable(value = "product", key = "#p0")

3. null 缓存

缓存 null 可以防穿透,但 JSON 序列化和业务语义要支持。建议用哨兵值或显式 value 对象。

4. 返回对象是代理/懒加载对象

MyBatis/JPA 动态代理序列化可能异常或体积异常。缓存 DTO,不要直接缓存 ORM 代理。

8.8 手写缓存模板

注解适合单对象方法,复杂业务建议显式控制:

public Product findWithCache(String id) {
    String key = "shop:product:" + id;
    Product cached = redisForObjects.opsForValue().get(key);
    if (cached != null) {
        return cached;
    }

    Product product = productMapper.findById(id)
            .orElseThrow(() -> new NotFoundException("product not found"));

    redisForObjects.opsForValue().set(
            key, product, Duration.ofSeconds(300 + randomTtl(60)));
    return product;
}

要点:

8.9 异常与降级

缓存故障不应拖垮主流程,但也不能静默吞掉:

public Product findWithFallback(String id) {
    try {
        Product cached = redis.opsForValue().get(key(id));
        if (cached != null) return cached;
    } catch (Exception e) {
        metrics.counter("redis.error").increment();
        log.warn("redis get failed, fallback db", e);
    }

    Product p = db.find(id);
    try {
        redis.opsForValue().set(key(id), p, Duration.ofMinutes(5));
    } catch (Exception e) {
        metrics.counter("redis.write.error").increment();
    }
    return p;
}

注意:

8.10 测试

@SpringBootTest
class RedisTemplateTest {

    @Autowired
    private StringRedisTemplate redis;

    @Test
    void shouldIncrement() {
        redis.delete("test:counter");
        assertThat(redis.opsForValue().increment("test:counter")).isEqualTo(1);
    }
}

集成测试可用 Testcontainers:

@Testcontainers
@SpringBootTest
class RedisIntegrationTest {
    @Container
    static GenericContainer<?> redis =
            new GenericContainer<>("redis:7.2").withExposedPorts(6379);
}

本章小结

思考题

  1. @Cacheable 为什么同类调用不生效?
  2. 缓存 null 与 disableCachingNullValues 分别适合什么场景?
  3. Redis 异常时是否所有业务都可以直接查库?哪些不能?