Spring Cache 中解决缓存穿透、缓存雪崩和缓存击穿的笔记
目录
引言
在使用Spring Cache时,缓存穿透、缓存雪崩和缓存击穿是常见的问题。这些问题可能导致系统性能下降,甚至影响系统的稳定性。本文将详细介绍如何在Spring Cache中解决这些问题,并以queryShopById
方法为例进行说明。
环境准备
为了实现以下解决方案,我们需要确保项目中已经引入了必要的依赖。假设我们使用的是Maven构建工具,可以在pom.xml
中添加以下依赖:
<dependencies>
<!-- Spring Boot Starter for Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter for Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson for JSON serialization -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Caffeine for local caching (optional) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Lombok for reducing boilerplate code (optional) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
同时,在application.yml
中配置Redis连接信息:
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
缓存穿透
问题描述:如果请求的id
对应的店铺不存在(即shopService.getById(id)
返回null),每次请求都会直接访问数据库,导致数据库压力增大。
解决方案:
缓存空对象:
- 通过
@Cacheable
注解的unless
属性或自定义逻辑来缓存null值或默认响应。
@RestController @RequestMapping("/shops") public class ShopController { @Autowired private ShopService shopService; @Cacheable(cacheNames = "shop:detail", key = "'shop'+#id", unless = "#result == null") public Result queryShopById(@PathVariable("id") Long id) { Shop shopDetail = shopService.getById(id); if (shopDetail == null) { // 如果需要缓存null值,可以在这里返回一个默认结果 return Result.ok(new Shop()); // 或者其他表示未找到的默认对象 } return Result.ok(shopDetail); } }
- 通过
布隆过滤器:
- 在服务层之前添加布隆过滤器检查,确保不存在的
id
不会到达数据库查询逻辑。
创建一个新的类
BloomFilterService
来管理布隆过滤器:package com.hmdp.service; import com.google.common.hash.Funnel; import com.google.common.hash.Hashing; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; import java.util.concurrent.ConcurrentHashMap; @Service public class BloomFilterService { private final ConcurrentHashMap<String, com.google.common.hash.BloomFilter<Long>> bloomFilters = new ConcurrentHashMap<>(); public void initBloomFilter(String cacheName, int expectedInsertions, double fpp) { Funnel<Long> funnel = (from, into) -> into.putString(Long.toString(from), StandardCharsets.UTF_8); bloomFilters.putIfAbsent(cacheName, com.google.common.hash.BloomFilter.create(funnel, expectedInsertions, fpp)); } public boolean mightContain(String cacheName, Long id) { com.google.common.hash.BloomFilter<Long> bloomFilter = bloomFilters.get(cacheName); return bloomFilter != null && bloomFilter.mightContain(id); } public void put(String cacheName, Long id) { com.google.common.hash.BloomFilter<Long> bloomFilter = bloomFilters.get(cacheName); if (bloomFilter != null) { bloomFilter.put(id); } } }
修改
ShopController
以使用布隆过滤器:@RestController @RequestMapping("/shops") public class ShopController { @Autowired private ShopService shopService; @Autowired private BloomFilterService bloomFilterService; @PostConstruct public void initBloomFilter() { bloomFilterService.initBloomFilter("shop:detail", 10000, 0.01); } @Cacheable(cacheNames = "shop:detail", key = "'shop'+#id", unless = "#result == null") public Result queryShopById(@PathVariable("id") Long id) { if (!bloomFilterService.mightContain("shop:detail", id)) { // 假设不存在于布隆过滤器中,直接返回404或其他错误信息 return Result.error("Shop not found"); } Shop shopDetail = shopService.getById(id); if (shopDetail != null) { bloomFilterService.put("shop:detail", id); // 将存在的ID加入布隆过滤器 } return Result.ok(shopDetail); } }
- 在服务层之前添加布隆过滤器检查,确保不存在的
缓存雪崩
问题描述:如果大量店铺详情数据在同一时间失效,可能导致短时间内所有请求都打到数据库上,造成数据库压力。
解决方案:
设置不同的过期时间:
- 使用
Caffeine
等支持过期策略的缓存提供者,并为每个缓存条目设置随机的过期时间。
修改
CacheMapper
配置类,创建一个具有自定义过期时间的RedisCacheManager
:package com.hmdp.config; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import java.time.Duration; import java.util.HashMap; import java.util.Map; /** * 配置类用于设置缓存的序列化方式和默认过期时间 */ @Configuration public class CacheMapper { /** * 根据传入的TTL值配置Redis缓存 * * @param ttl 缓存的存活时间(秒) * @return RedisCacheConfiguration 对象 */ private RedisCacheConfiguration instanceConfig(Long ttl) { // 使用Jackson进行序列化 Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); // 确保日期类型以ISO-8601格式序列化 objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 注册Java Time模块以支持Java 8日期/时间API objectMapper.registerModule(new JavaTimeModule()); // 忽略注解,避免性能问题 objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false); // 只序列化非空对象 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 启用类型信息的默认处理 objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 配置默认缓存项的存活时间和序列化方式 return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(ttl)) .disableCachingNullValues() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)); } /** * 创建一个具有自定义过期时间的Redis缓存管理器 * * @param connectionFactory Redis连接工厂 * @param customCacheTimes Map<String, Long>,键为缓存名称,值为对应的TTL(秒) * @return RedisCacheManager 对象 */ @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory, @Value("#{${custom.cache-times:}}") Map<String, Long> customCacheTimes) { // 默认配置 RedisCacheConfiguration defaultConfig = instanceConfig(3600L); // 默认1小时 // 自定义缓存配置 Map<String, RedisCacheConfiguration> customCacheConfigurations = new HashMap<>(); customCacheTimes.forEach((cacheName, ttl) -> { customCacheConfigurations.put(cacheName, instanceConfig(ttl)); }); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(customCacheConfigurations) .transactionAware() .build(); } }
在
application.yml
中配置自定义缓存时间:spring: cache: type: redis redis: time-to-live: 3600s # 默认1小时 custom: cache-times: shop:detail: 600 # shop:detail缓存存活10分钟 product:info: 300 # product:info缓存存活5分钟
- 使用
热点数据永不过期:
- 对于特别重要的店铺详情数据,可以考虑不设置过期时间,或者设置非常长的过期时间,并通过后台任务定期更新缓存。
@Cacheable(cacheNames = "shop:detail", key = "'shop'+#id", cacheManager = "neverExpiringCacheManager") public Result queryShopById(@PathVariable("id") Long id) { Shop shopDetail = shopService.getById(id); return Result.ok(shopDetail); } @Bean(name = "neverExpiringCacheManager") public RedisCacheManager neverExpiringCacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = instanceConfig(365 * 24 * 3600L); // 设置一年的过期时间 return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .transactionAware() .build(); }
多级缓存架构:
- 结合本地缓存(如
Caffeine
)和分布式缓存(如Redis)来构建多级缓存体系。
创建一个新的类
MultiLevelCacheResolver
来实现多级缓存解析器:package com.hmdp.config; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.Optional; @Component public class MultiLevelCacheResolver implements CacheResolver { private final CacheManager localCacheManager; private final CacheManager distributedCacheManager; @Autowired public MultiLevelCacheResolver(CacheManager localCacheManager, CacheManager distributedCacheManager) { this.localCacheManager = localCacheManager; this.distributedCacheManager = distributedCacheManager; } @Override public Collection<Cache> resolveCaches(CacheOperationInvocationContext<?> context) { String cacheName = context.getCacheNames().iterator().next(); Cache localCache = localCacheManager.getCache(cacheName); Cache distributedCache = distributedCacheManager.getCache(cacheName); // 先尝试从本地缓存获取 if (localCache != null && localCache.get(context.getKey()) != null) { return Collections.singletonList(localCache); } // 如果本地缓存未命中,再尝试从分布式缓存获取 if (distributedCache != null) { Object value = distributedCache.get(context.getKey(), Object.class); if (value != null) { // 将分布式缓存中的数据同步到本地缓存 localCache.put(context.getKey(), value); } return Collections.singletonList(distributedCache); } return Collections.emptyList(); } }
修改
ShopController
以使用多级缓存解析器:@RestController @RequestMapping("/shops") public class ShopController { @Autowired private ShopService shopService; @Cacheable(cacheNames = "shop:detail", key = "'shop'+#id", cacheResolver = "multiLevelCacheResolver") public Result queryShopById(@PathVariable("id") Long id) { Shop shopDetail = shopService.getById(id); return Result.ok(shopDetail); } }
- 结合本地缓存(如
缓存击穿
问题描述:对于热门店铺,当其缓存失效时,可能会有大量并发请求同时查询该店铺的数据,给数据库带来巨大压力。
解决方案:
互斥锁机制:
- 使用同步机制确保同一时刻只有一个线程能够更新缓存。可以通过
sync = true
属性或手动实现同步逻辑。
使用
sync = true
:@RestController @RequestMapping("/shops") public class ShopController { @Autowired private ShopService shopService; @Cacheable(cacheNames = "shop:detail", key = "'shop'+#id", sync = true) public Result queryShopById(@PathVariable("id") Long id) { Shop shopDetail = shopService.getById(id); return Result.ok(shopDetail); } }
手动实现同步逻辑:
创建一个新的类
ShopCacheService
来管理缓存和同步逻辑:package com.hmdp.service; import com.hmdp.config.CacheMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @Service public class ShopCacheService { private final Map<Long, Lock> lockMap = new ConcurrentHashMap<>(); private final ShopService shopService; private final CacheManager cacheManager; @Autowired public ShopCacheService(ShopService shopService, CacheManager cacheManager) { this.shopService = shopService; this.cacheManager = cacheManager; } @Cacheable(cacheNames = "shop:detail", key = "'shop'+#id", unless = "#result == null") public Result queryShopById(Long id) { Lock lock = lockMap.computeIfAbsent(id, k -> new ReentrantLock()); try { // 尝试获取锁 if (lock.tryLock()) { try { // 再次检查缓存,防止重复加载 Result cachedResult = getFromCache(id); if (cachedResult != null) { return cachedResult; } // 执行实际的数据加载逻辑 Shop shopDetail = shopService.getById(id); putToCache(id, Result.ok(shopDetail)); // 更新缓存 return Result.ok(shopDetail); } finally { lock.unlock(); // 确保释放锁 } } else { // 如果无法获取锁,等待一段时间后再尝试 Thread.sleep(100); // 可以根据需要调整等待时间 return queryShopById(id); // 递归调用,再次尝试获取锁 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for lock", e); } } private Result getFromCache(Long id) { // 实现从缓存获取逻辑 return (Result) cacheManager.getCache("shop:detail").get(id, Result.class); } private void putToCache(Long id, Result result) { // 实现向缓存写入逻辑 cacheManager.getCache("shop:detail").put(id, result); } }
修改
ShopController
以使用ShopCacheService
:@RestController @RequestMapping("/shops") public class ShopController { @Autowired private ShopCacheService shopCacheService; @GetMapping("/{id}") public Result queryShopById(@PathVariable("id") Long id) { return shopCacheService.queryShopById(id); } }
- 使用同步机制确保同一时刻只有一个线程能够更新缓存。可以通过
预热机制:
- 编写定时任务,在缓存即将过期前主动加载数据,确保缓存中的数据始终保持有效。
创建一个新的类
ShopPreloadTask
来实现定时任务:package com.hmdp.task; import com.hmdp.service.ShopService; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; @Component public class ShopPreloadTask { private final ShopService shopService; @Autowired public ShopPreloadTask(ShopService shopService) { this.shopService = shopService; } @Scheduled(cron = "0 0/1 * * * ?") // 每分钟执行一次 public void preloadHotShops() { List<Long> hotShopIds = shopService.findTop10HotShopIds(); // 获取热门店铺ID for (Long id : hotShopIds) { shopService.getById(id); // 触发缓存加载 } } }
双层缓存:
- 如前所述,结合本地缓存和分布式缓存,可以有效缓解缓存击穿问题。具体的实现方式已经在缓存雪崩部分提到,这里不再赘述。
总结
通过上述优化,可以有效地解决queryShopById
方法中可能出现的缓存穿透、缓存雪崩和缓存击穿问题。根据具体的业务需求和技术栈,选择合适的策略来优化缓存机制,是提高系统性能和稳定性的关键。