这是《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);
}
}
适合:
- 计数器;
- 分布式锁;
- JSON 字符串;
- 与非 Java 客户端共享的数据。
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;
}
要点:
- TTL 加随机值防雪崩;
- miss 时做并发控制(第 25 章);
- 数据库更新后按一致性策略失效;
- 指标记录命中率、miss 率、回源耗时。
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;
}
注意:
- 强依赖数据(锁、库存、token)不能简单降级;
- 降级后数据库要有限流;
- 每次降级必须可观测。
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);
}
本章小结
- Spring Boot 3 默认 Lettuce,连接池需 commons-pool2;
- RedisTemplate 必须显式配置 key/value 序列化;
StringRedisTemplate适合计数、锁和跨语言字符串;- 缓存注解要理解 AOP 代理与 key 表达式;
- 生产缓存建议显式 TTL、随机化、指标和降级策略。
思考题
@Cacheable为什么同类调用不生效?- 缓存 null 与
disableCachingNullValues分别适合什么场景? - Redis 异常时是否所有业务都可以直接查库?哪些不能?