为什么需要缓存
数据库是大多数 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 加随机抖动、布隆过滤器/空值缓存、分布式锁或不过期策略来应对。
三大缓存问题的代码防护
缓存穿透(Cache Penetration)
查询数据库中根本不存在的数据(如 id=-1),缓存永远未命中,每次都打到数据库。解决:查询结果为空时也缓存空值(设置较短 TTL);或使用布隆过滤器(Bloom Filter)在缓存层前过滤明显不存在的请求。
缓存雪崩(Cache Avalanche)
大量缓存 key 在同一时刻同时过期(如促销活动结束),请求全部穿透到数据库,造成数据库压力剧增甚至宕机。解决:TTL 加随机抖动(在基础 TTL 上加 ±N 秒随机值),使各 key 过期时间分散。
缓存击穿(Cache Breakdown)
某个极热门的 key 过期瞬间,大量并发请求同时发现缓存未命中,全部去查数据库,相当于"并发的缓存穿透"。解决:互斥锁(Mutex Lock)——只允许一个线程查询数据库并重建缓存,其他线程等待;或设置热点 key 永不过期(逻辑过期)。
// 防缓存穿透:空值缓存 + 短 TTL
@Cacheable(value = "users", key = "#id", unless = "false")
public UserResponseDTO findById(Long id) {
return userRepository.findById(id)
.map(this::toDTO)
.orElse(null); // 返回 null 也会被缓存(unless = "false" 表示始终缓存)
}
// TTL 加随机抖动(防雪崩)
public RedisCacheConfiguration withJitter(Duration baseTtl) {
long jitterSeconds = new Random().nextLong(60); // ±60 秒随机
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(baseTtl.plusSeconds(jitterSeconds));
}
// 互斥锁防击穿(只允许一个请求重建缓存)
public UserResponseDTO findByIdWithMutex(Long id) throws InterruptedException {
String cacheKey = "users::" + id;
String lockKey = "lock:users:" + id;
// 1. 先查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return (UserResponseDTO) cached;
// 2. 缓存未命中,尝试获取互斥锁(setIfAbsent = SETNX)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 获锁成功:查 DB,写缓存
UserResponseDTO dto = userRepository.findById(id)
.map(this::toDTO).orElse(null);
redisTemplate.opsForValue().set(cacheKey, dto, Duration.ofMinutes(10));
return dto;
} finally {
redisTemplate.delete(lockKey); // 释放锁
}
} else {
// 4. 未获锁:短暂等待后重试
Thread.sleep(50);
return findByIdWithMutex(id);
}
}
本章小结
Spring Cache 抽象提供了声明式缓存(@Cacheable/@CachePut/@CacheEvict),通过切换 CacheManager 可以无缝更换缓存后端;RedisTemplate 适合需要精细控制的场景(计数器、排行榜、分布式锁)。缓存一致性最佳策略是 Cache Aside(先写 DB,再删缓存);生产环境必须防范缓存穿透(空值缓存)、雪崩(TTL 抖动)、击穿(互斥锁或逻辑过期)三大问题。