4-4 缓存集成
概念解析
Spring Cache 注解
| 注解 | 说明 |
|---|---|
| @EnableCaching | 开启缓存支持 |
| @Cacheable | 方法结果放入缓存 |
| @CachePut | 更新缓存(不影响方法执行) |
| @CacheEvict | 清除缓存 |
| @Caching | 组合多个缓存操作 |
缓存策略
| 策略 | 说明 | 使用场景 |
|---|---|---|
| ALL | 缓存所有数据 | 数据量小、变化少 |
| Cache-aside | 先查缓存,后查库 | 大部分场景 |
| Read-through | 缓存负责加载数据 | 预热场景 |
| Write-through | 同步写缓存和库 | 数据一致性要求高 |
| Write-behind | 异步批量写库 | 高并发写场景 |
代码示例
1. Redis 配置
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON 序列化
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// 配置缓存管理器
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // 默认过期时间
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withCacheConfiguration("users",
config.entryTtl(Duration.ofMinutes(30)))
.withCacheConfiguration("products",
config.entryTtl(Duration.ofHours(2)))
.build();
}
}
2. 基本缓存使用
@Service
@CacheConfig(cacheNames = "users") // 类级别统一配置
public class UserService {
@Cacheable(key = "#id", unless = "#result == null")
public User getUser(Long id) {
log.info("查询数据库: {}", id);
return userRepository.findById(id).orElse(null);
}
@CachePut(key = "#result.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(allEntries = true)
public void refreshAll() {
// 清除所有用户缓存
}
}
3. 复杂缓存策略
@Service
public class ProductService {
// 条件缓存
@Cacheable(
key = "#id",
condition = "#id > 100", // id > 100 才缓存
unless = "#result.soldOut == true" // 已售罄不缓存
)
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// 多级缓存
@Cacheable(
value = {"localCache", "redisCache"},
key = "#key",
cacheManager = "cacheManager"
)
public Object getWithMultiCache(String key) {
return databaseService.find(key);
}
// 组合缓存操作
@Caching(
evict = {
@CacheEvict(key = "#result.id"),
@CacheEvict(value = "user-products", key = "#result.userId")
}
)
public Product updateProduct(Product product) {
return productRepository.save(product);
}
}
4. Redis 缓存实现
@Service
public class RedisCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CACHE_PREFIX = "cache:";
public <T> T get(String key, Class<T> type) {
String cacheKey = CACHE_PREFIX + key;
Object value = redisTemplate.opsForValue().get(cacheKey);
return value != null ? type.cast(value) : null;
}
public void set(String key, Object value, long seconds) {
String cacheKey = CACHE_PREFIX + key;
redisTemplate.opsForValue().set(cacheKey, value,
Duration.ofSeconds(seconds));
}
public void delete(String key) {
redisTemplate.delete(CACHE_PREFIX + key);
}
public Boolean hasKey(String key) {
return redisTemplate.hasKey(CACHE_PREFIX + key);
}
public void expire(String key, long seconds) {
redisTemplate.expire(CACHE_PREFIX + key, Duration.ofSeconds(seconds));
}
}
5. 分布式锁
@Service
public class DistributedLockService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LOCK_PREFIX = "lock:";
public boolean tryLock(String key, long expireSeconds) {
String lockKey = LOCK_PREFIX + key;
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(result);
}
public void unlock(String key) {
String lockKey = LOCK_PREFIX + key;
redisTemplate.delete(lockKey);
}
public <T> T executeWithLock(String key, long timeout,
Supplier<T> supplier) {
if (tryLock(key, timeout)) {
try {
return supplier.get();
} finally {
unlock(key);
}
}
throw new RuntimeException("获取锁失败");
}
}
// 使用
@Service
public class StockService {
@Autowired
private DistributedLockService lockService;
public void decreaseStock(Long productId, int quantity) {
String lockKey = "stock:" + productId;
lockService.executeWithLock(lockKey, 10, () -> {
Stock stock = stockRepository.findByProductId(productId);
stock.setQuantity(stock.getQuantity() - quantity);
stockRepository.save(stock);
return null;
});
}
}
6. 缓存雪崩/击穿处理
@Service
public class CacheStrategyService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 缓存雪崩:过期时间加随机值
public void setWithRandomExpire(String key, Object value,
long baseSeconds) {
long actualSeconds = baseSeconds + new Random().nextInt(300);
redisTemplate.opsForValue().set(key, value,
Duration.ofSeconds(actualSeconds));
}
// 缓存击穿:分布式锁
public Product getProductWithLock(Long id) {
String cacheKey = "product:" + id;
Product product = getFromCache(cacheKey);
if (product != null) {
return product;
}
String lockKey = "lock:" + cacheKey;
if (lockService.tryLock(lockKey, 10)) {
try {
// 双重检查
product = getFromCache(cacheKey);
if (product != null) {
return product;
}
// 从数据库加载
product = productRepository.findById(id).orElse(null);
if (product != null) {
saveToCache(cacheKey, product, 3600);
}
} finally {
lockService.unlock(lockKey);
}
}
return product;
}
// 缓存穿透:布隆过滤器 或 空值缓存
public User getUser(Long id) {
String cacheKey = "user:" + id;
// 空值缓存,防止穿透
String nullValue = (String) redisTemplate.opsForValue().get(cacheKey + ":null");
if ("1".equals(nullValue)) {
return null;
}
User user = getFromCache(cacheKey);
if (user == null) {
user = userRepository.findById(id).orElse(null);
if (user == null) {
// 缓存空值,短时间有效
redisTemplate.opsForValue().set(
cacheKey + ":null", "1", Duration.ofMinutes(5));
} else {
saveToCache(cacheKey, user, 3600);
}
}
return user;
}
}
常见坑点
⚠️ 坑 1:缓存和数据库不一致
// ❌ 先删缓存,后更新数据库(高并发下可能读到旧数据)
@CacheEvict(key = "#id")
public void update(User user) {
userRepository.save(user); // 如果此时其他请求查库,会写入旧缓存
}
// ✅ 方案1:延迟双删
public void update(User user) {
redisTemplate.delete("user:" + user.getId());
userRepository.save(user);
try { Thread.sleep(100); } catch (Exception e) {}
redisTemplate.delete("user:" + user.getId());
}
// ✅ 方案2:分布式事务(最终一致)
// ✅ 方案3:更新时不清缓存,只删除
⚠️ 坑 2:@Cacheable 不生效
// ❌ 类内部调用不走代理
@Service
public class UserService {
public User getById(Long id) {
return getFromDb(id); // 不会走缓存
}
@Cacheable(key = "#id")
public User getFromDb(Long id) {
return userRepository.findById(id).orElse(null);
}
}
// ✅ 注入自身代理
@Autowired
private UserService self;
public User getById(Long id) {
return self.getFromDb(id); // 走代理
}
⚠️ 坑 3:序列化问题
// ❌ 直接存储未实现序列化
@Cacheable(key = "#user.id")
public void cacheUser(User user) { // User 必须实现 Serializable
}
// ✅ 确保实体可序列化
@Entity
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
面试题
Q1:缓存穿透、击穿、雪崩的区别?
参考答案:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 穿透 | 查询不存在的数据 | 布隆过滤器 / 空值缓存 |
| 击穿 | 热点 key 过期瞬间大量请求 | 分布式锁 / 永不过期 |
| 雪崩 | 大量 key 同时过期 | 过期时间随机 / 多级缓存 |
Q2:Redis 和本地缓存如何选择?
参考答案:
| 维度 | Redis | 本地缓存(Caffeine/Guava) |
|---|---|---|
| 共享 | 多实例共享 | 各实例独立 |
| 容量 | 受内存限制 | 受 JVM 内存限制 |
| 性能 | 略慢(网络) | 更快(本地) |
| 适用 | 分布式 / 大数据量 | 热点数据 / 低延迟 |
建议:二级缓存组合使用
请求 → 本地缓存 → Redis → 数据库
Q3:如何保证缓存与数据库一致性?
参考答案:
-
Cache-aside(旁路缓存)
- 读:缓存存在返回,不存在查库并写入缓存
- 写:先更新数据库,再删除缓存
-
延迟双删
- 更新时先删缓存
- 更新数据库
- 延迟一段时间后再删缓存
-
订阅 Binlog
- 监听数据库变更
- 异步更新缓存
-
分布式事务(强一致,但不推荐,性能差)