缓存抽象与 Redis
本教程共 48 篇 · 第 31 篇 · 更新于 2026-08-13 · 约 6 分钟阅读
本节目标:理解缓存抽象的原理,会用 @Cacheable/@CachePut/@CacheEvict 注解,能把缓存后端从内存换成 Redis。
缓存解决什么问题
商品详情这种数据,读多写少,每次请求都查数据库,慢还费资源。缓存把热点数据放在更快的地方(内存、Redis),下次直接取,数据库压力骤降。
判断一个数据适不适合缓存,问两个问题:读的次数远大于写的次数吗?数据变了一点不实时也能接受吗?两个都答是,就值得缓存。库存这种强一致数据,别碰缓存。
Spring 的缓存抽象把「存哪」和「怎么用」解耦了。你只写注解声明哪些方法要缓存,具体存内存还是 Redis,由 CacheManager 决定,换后端不用改业务代码。
缓存更新的经典套路
缓存不是把数据放进去就完事,难点在更新。业界最常用的是 Cache Aside(旁路缓存)策略:
- 读:先查缓存,命中直接返回。
- 未命中:查数据库,写回缓存,返回。
- 写:先更新数据库,再删除缓存。
先更新数据库、再删缓存,是最稳的顺序。反过来先删缓存再更新数据库,中间有并发窗口,可能把旧数据写回缓存。
开启缓存:@EnableCaching
先引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
然后在配置类上开启缓存支持:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
}
Note官方文档特意提醒:别把
@EnableCaching加到主类上。主类被测试、各种场景共享,缓存会变成强制依赖,测试跑起来反而麻烦。放到独立的配置类里,清爽又可控。
@Cacheable:读缓存
给查询方法加 @Cacheable,结果自动进缓存:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable("products")
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
}
第一次调用:方法执行,结果存入名为 products 的缓存。第二次用同样的 id 调用:直接返回缓存,方法体不执行。
默认的缓存键就是方法参数。多个参数或想自定义键,用 key 属性:
@Cacheable(value = "products", key = "#id + '-' + #lang")
public Product findById(Long id, String lang) {
return productRepository.findById(id).orElseThrow();
}
key 里写的是 SpEL 表达式,#参数名 取方法参数。注意:@Cacheable 默认不缓存 null 返回值。方法返回 null 时每次都重新执行,想缓存空值结果(防缓存穿透的一种手段),设置 cache-null-values 或 unless = "#result == null" 配合使用。
条件缓存也常用:数据量小时不缓存,数据量大时才缓存。
// 价格超过 100 的才进缓存
@Cacheable(value = "products", condition = "#price > 100")
public Product findExpensive(Long id, BigDecimal price) {
return productRepository.findById(id).orElseThrow();
}
@CachePut 与 @CacheEvict:更新与删除
写操作要同步缓存,两个注解分工:
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
@Service
public class ProductService {
// 更新方法执行完,把返回值写进缓存(覆盖旧值)
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}
// 删除时把对应缓存也删掉,符合 Cache Aside 的"更新先删缓存"
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepository.deleteById(id);
}
}
区别记住一句话:@CachePut 方法总是执行,执行后写缓存;@CacheEvict 执行后删缓存,删哪个由 key 决定。批量删除用 allEntries = true,清空整个缓存区:
@CacheEvict(value = "products", allEntries = true)
public void deleteAll() {
productRepository.deleteAll();
}
一个方法要同时触发多个缓存操作时,用 @Caching 组合:
@Caching(
put = @CachePut(value = "products", key = "#product.id"),
evict = @CacheEvict(value = "productLists", allEntries = true)
)
public Product update(Product product) {
return productRepository.save(product);
}
更新商品时:详情缓存写入新值,列表缓存整体清掉。两个动作一次搞定。
Tip同一类里
this.update()自调用,注解不生效——缓存注解和事务注解一样走代理,必须从外部调用。调用方和被调用方分开写,是常见的最佳实践。
默认缓存:ConcurrentMapCacheManager
不加任何缓存库时,Spring Boot 自动配一个基于 ConcurrentHashMap 的简单缓存管理器。它开箱即用,但只在单机内存里,重启丢失,不适合生产,适合学习阶段。
预声明缓存名可以防止拼写错误:
spring:
cache:
cache-names: products, categories
声明后,注解里用了没声明的缓存名,运行时直接报错,拼写错误当场暴露,不会悄悄建一个没人用的缓存。
想临时关闭缓存(比如排查问题),一行配置切到空实现:
spring:
cache:
type: none
业务代码一行不改,缓存全部失效,适合对比「有没有缓存」的差异。
换成 Redis:分布式缓存
多实例部署时,每个实例的内存缓存各管各的,数据不一致。Redis 是共享的,所有实例读写同一个缓存。
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Note老教程里的
spring-boot-starter-redis早已不存在,现在的名字是spring-boot-starter-data-redis。照抄旧依赖会直接构建失败。
配置连接和缓存参数:
spring:
data:
redis:
host: localhost
port: 6379
database: 0 # 默认 0,多业务可以分库
# password: 你的密码 # 有密码就打开
cache:
type: redis # 指定缓存后端为 Redis
redis:
time-to-live: 10m # 缓存 10 分钟过期
cache-null-values: false
classpath 里有 Redis 相关类,Spring Boot 就自动配 RedisCacheManager。默认客户端是 Lettuce,高性能且线程安全,不用额外配置。连接池要自己开:引入 commons-pool2 依赖后,用 spring.data.redis.lettuce.pool.* 配池参数。
time-to-live 设置过期时间,防止缓存永远不失效。
启动后,@Cacheable("products") 的键值对就存进 Redis 了,多个实例共享同一份缓存。默认会给每个缓存加前缀,避免不同缓存间键冲突,这个设计保持默认就好。
缓存对象要能序列化
Redis 里存的是字节。缓存的对象必须可序列化,否则存的时候直接报错:
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
// 字段、getter、setter
}
Warning默认的 JDK 序列化存进 Redis 的是二进制,
redis-cli里看是一堆乱码,不好排查。想让 Redis 里存 JSON,需要自定义RedisCacheConfiguration配GenericJackson2JsonRedisSerializer,进阶阶段再展开。
手动操作:RedisTemplate
注解管不到的临时操作,用 RedisTemplate 直接读写:
import org.springframework.data.redis.core.StringRedisTemplate;
@Service
public class VisitCounter {
private final StringRedisTemplate redisTemplate;
public VisitCounter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void visit(Long productId) {
redisTemplate.opsForValue().increment("visit:" + productId);
}
public String count(Long productId) {
return redisTemplate.opsForValue().get("visit:" + productId);
}
}
opsForValue() 是字符串操作视图,还有 opsForList()、opsForHash()、opsForSet() 对应 Redis 的各类数据结构。StringRedisTemplate 是字符串专用版,最常用。
设值的时候可以带过期时间:
redisTemplate.opsForValue().set("token:" + userId, token, 30, TimeUnit.MINUTES);
30 分钟自动过期,不用手动删,这是 Redis 相比内存缓存的一大优势。
小结
缓存抽象把「存哪」和「怎么用」分开:注解声明缓存行为,CacheManager 决定存储后端。入门用内存缓存,生产上 Redis,换后端业务代码一行不改。Cache Aside 策略记住:更新先写库、再删缓存。
最后留三个高频词:缓存穿透(查不存在的数据)、缓存击穿(热点 key 失效)、缓存雪崩(大量 key 同时失效)。这『三兄弟』是缓存面试必问,本节先把概念记住,进阶再看解决方案。
下一节发邮件:JavaMailSender 的简单用法。