为什么需要缓存
数据库是大多数 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 加随机抖动、布隆过滤器/空值缓存、分布式锁或不过期策略来应对。