引言: 在高并发场景下,缓存系统已成为提升系统性能的关键手段。然而,单一的分布式缓存方案(如Redis)在面对极高并发请求时,仍可能面临网络延迟、缓存穿透等挑战。本文将介绍在"黑马点评"项目中实现的二级缓存优化方案,通过Spring Cache抽象层整合Caffeine本地缓存与Redis分布式缓存,有效提升系统吞吐能力。
二级缓存方案设计: 二级缓存架构采用"本地缓存+分布式缓存"的双层缓存策略,其核心思想是:将高频访问的热点数据同时存储在本地内存和分布式缓存中,使大多数请求能够直接从本地内存获取数据,大幅降低网络IO开销。 技术栈选择Spring Cache:提供统一的缓存抽象接口,简化缓存操作Caffeine:高性能本地缓存库,提供近似最优的缓存淘汰策略Redis:分布式缓存,保障数据一致性与集群共享核心实现缓存管理器配置(CacheConfig)下面是完整的CacheConfid代码 我是放在com.hmdp.config包下 package com.hmdp.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.github.benmanes.caffeine.cache.Caffeine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCacheManager; 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.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
@Configuration @EnableCaching public class CacheConfig {
private static final Logger logger = LoggerFactory.getLogger(CacheConfig.class);
@Value("${spring.application.name:hmdp}")
private String appName;
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
logger.info("初始化二级缓存管理器...");
// 创建Redis缓存管理器
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(getRedisCacheConfigurationWithTtl(3600)) // 默认1小时过期
.withCacheConfiguration("userCache", getRedisCacheConfigurationWithTtl(1800)) // 用户缓存30分钟
.withCacheConfiguration("productCache", getRedisCacheConfigurationWithTtl(7200)) // 产品缓存2小时
.withCacheConfiguration("shopCache", getRedisCacheConfigurationWithTtl(1800)) // 新增:店铺缓存30分钟
.build();
logger.info("Redis缓存管理器初始化完成,已配置缓存: userCache(30min), productCache(2h), shopCache(30min)");
// 创建Caffeine缓存管理器
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(1000) // 最大容量
.expireAfterWrite(5, TimeUnit.MINUTES) // 写入后5分钟过期
.recordStats()); // 开启统计
logger.info("Caffeine本地缓存管理器初始化完成,配置: 初始容量100, 最大容量1000, 过期时间5分钟");
// 创建二级缓存管理器
LayeringCacheManager layeringCacheManager = new LayeringCacheManager(caffeineCacheManager, redisCacheManager);
logger.info("二级缓存管理器初始化完成,采用Caffeine本地缓存 + Redis远程缓存架构");
return layeringCacheManager;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(long seconds) {
logger.debug("创建Redis缓存配置,TTL: {}秒", seconds);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(seconds))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.computePrefixWith(cacheName -> {
String prefix = appName + ":" + cacheName + ":";
logger.trace("缓存前缀: {}", prefix);
return prefix;
});
}
// 二级缓存管理器实现
public static class LayeringCacheManager implements CacheManager {
private static final Logger logger = LoggerFactory.getLogger(LayeringCacheManager.class);
private final CacheManager localCacheManager;
private final CacheManager remoteCacheManager;
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
public LayeringCacheManager(CacheManager localCacheManager, CacheManager remoteCacheManager) {
this.localCacheManager = localCacheManager;
this.remoteCacheManager = remoteCacheManager;
logger.info("创建二级缓存管理器,本地缓存: {}, 远程缓存: {}",
localCacheManager.getClass().getSimpleName(),
remoteCacheManager.getClass().getSimpleName());
}
@Override
public Cache getCache(String name) {
logger.debug("获取缓存实例: {}", name);
return cacheMap.computeIfAbsent(name, cacheName -> {
Cache localCache = localCacheManager.getCache(cacheName);
Cache remoteCache = remoteCacheManager.getCache(cacheName);
if (localCache == null) {
logger.warn("本地缓存管理器未找到缓存: {}", cacheName);
}
if (remoteCache == null) {
logger.warn("远程缓存管理器未找到缓存: {}", cacheName);
}
LayeringCache layeringCache = new LayeringCache(localCache, remoteCache);
logger.info("创建二级缓存实例: {}", cacheName);
return layeringCache;
});
}
@Override
public Collection<String> getCacheNames() {
Set<String> names = new LinkedHashSet<>();
names.addAll(localCacheManager.getCacheNames());
names.addAll(remoteCacheManager.getCacheNames());
logger.debug("获取所有缓存名称,本地缓存: {}, 远程缓存: {}, 合并后: {}",
localCacheManager.getCacheNames().size(),
remoteCacheManager.getCacheNames().size(),
names.size());
return names;
}
// 二级缓存实现
static class LayeringCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(LayeringCache.class);
private final Cache localCache;
private final Cache remoteCache;
private final String name;
public LayeringCache(Cache localCache, Cache remoteCache) {
this.localCache = localCache;
this.remoteCache = remoteCache;
this.name = localCache != null ? localCache.getName() : "unknown";
logger.debug("创建LayeringCache实例: {}", this.name);
}
@Override
public String getName() {
return name;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(Object key) {
logger.debug("【二级缓存】查询 - 缓存名: {}, 键: {}", name, key);
// 先查本地缓存
ValueWrapper wrapper = localCache.get(key);
if (wrapper != null) {
logger.debug("【二级缓存】本地缓存命中 - 缓存名: {}, 键: {}", name, key);
return wrapper;
}
logger.debug("【二级缓存】本地缓存未命中,查询远程缓存 - 缓存名: {}, 键: {}", name, key);
// 本地未命中,查远程缓存
wrapper = remoteCache.get(key);
if (wrapper != null) {
logger.debug("【二级缓存】远程缓存命中,回填本地缓存 - 缓存名: {}, 键: {}", name, key);
Object value = wrapper.get();
// 回填本地缓存
localCache.put(key, value);
} else {
logger.debug("【二级缓存】远程缓存未命中 - 缓存名: {}, 键: {}", name, key);
}
return wrapper;
}
@Override
public <T> T get(Object key, Class<T> type) {
logger.debug("【二级缓存】查询(带类型) - 缓存名: {}, 键: {}, 类型: {}", name, key, type.getSimpleName());
// 先查本地缓存
T value = localCache.get(key, type);
if (value != null) {
logger.debug("【二级缓存】本地缓存命中(带类型) - 缓存名: {}, 键: {}", name, key);
return value;
}
logger.debug("【二级缓存】本地缓存未命中(带类型),查询远程缓存 - 缓存名: {}, 键: {}", name, key);
// 本地未命中,查远程缓存
value = remoteCache.get(key, type);
if (value != null) {
logger.debug("【二级缓存】远程缓存命中(带类型),回填本地缓存 - 缓存名: {}, 键: {}", name, key);
// 回填本地缓存
localCache.put(key, value);
} else {
logger.debug("【二级缓存】远程缓存未命中(带类型) - 缓存名: {}, 键: {}", name, key);
}
return value;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
logger.debug("【二级缓存】查询(加载器) - 缓存名: {}, 键: {}", name, key);
// 先查本地缓存
try {
T value = localCache.get(key, () -> {
logger.debug("【二级缓存】本地缓存未命中(加载器),查询远程缓存 - 缓存名: {}, 键: {}", name, key);
// 本地未命中,查远程缓存
try {
return remoteCache.get(key, valueLoader);
} catch (Exception e) {
logger.warn("【二级缓存】远程缓存查询异常,执行valueLoader加载数据 - 缓存名: {}, 键: {}, 异常: {}",
name, key, e.getMessage());
// 远程缓存未命中或异常,执行valueLoader加载数据
T newValue = valueLoader.call();
if (newValue != null) {
logger.debug("【二级缓存】数据加载成功,填充远程缓存 - 缓存名: {}, 键: {}", name, key);
remoteCache.put(key, newValue); // 填充远程缓存
} else {
logger.debug("【二级缓存】数据加载返回null,不缓存 - 缓存名: {}, 键: {}", name, key);
}
return newValue;
}
});
if (value != null) {
logger.debug("【二级缓存】查询成功(加载器) - 缓存名: {}, 键: {}", name, key);
} else {
logger.debug("【二级缓存】查询返回null(加载器) - 缓存名: {}, 键: {}", name, key);
}
return value;
} catch (Exception e) {
logger.error("【二级缓存】本地缓存异常,尝试直接读远程缓存 - 缓存名: {}, 键: {}, 异常: {}",
name, key, e.getMessage());
// 本地缓存异常,尝试直接读远程缓存
try {
return remoteCache.get(key, valueLoader);
} catch (Exception ex) {
logger.error("【二级缓存】远程缓存也异常 - 缓存名: {}, 键: {}, 异常: {}",
name, key, ex.getMessage());
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new IllegalStateException(ex);
}
}
}
@Override
public void put(Object key, Object value) {
logger.debug("【二级缓存】写入 - 缓存名: {}, 键: {}, 值类型: {}",
name, key, value != null ? value.getClass().getSimpleName() : "null");
remoteCache.put(key, value); // 先放入远程缓存
logger.debug("【二级缓存】远程缓存写入完成 - 缓存名: {}, 键: {}", name, key);
localCache.put(key, value); // 再放入本地缓存
logger.debug("【二级缓存】本地缓存写入完成 - 缓存名: {}, 键: {}", name, key);
}
@Override
public void evict(Object key) {
logger.debug("【二级缓存】逐出 - 缓存名: {}, 键: {}", name, key);
remoteCache.evict(key); // 先清远程缓存
logger.debug("【二级缓存】远程缓存逐出完成 - 缓存名: {}, 键: {}", name, key);
localCache.evict(key); // 再清本地缓存
logger.debug("【二级缓存】本地缓存逐出完成 - 缓存名: {}, 键: {}", name, key);
}
@Override
public void clear() {
logger.info("【二级缓存】清空所有 - 缓存名: {}", name);
remoteCache.clear(); // 先清远程缓存
logger.debug("【二级缓存】远程缓存清空完成 - 缓存名: {}", name);
localCache.clear(); // 再清本地缓存
logger.debug("【二级缓存】本地缓存清空完成 - 缓存名: {}", name);
}
}
}
} 具体实现: 我是在ShopServiceImpl 加入二级缓存, 下面是我的ShopServiceImpl 的完整代码,这个代码中还有一个我写的缓存预热,这里我就不展开说了
package com.hmdp.service.impl;
import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.dto.Result; import com.hmdp.entity.Shop; import com.hmdp.mapper.ShopMapper; import com.hmdp.service.IShopService; import com.hmdp.utils.SystemConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.redis.connection.RedisGeoCommands; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.domain.geo.GeoReference; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.SHOP_GEO_KEY;
@Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
private static final Logger logger = LoggerFactory.getLogger(ShopServiceImpl.class);
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private CacheManager cacheManager;
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 线程池用于异步缓存预热
private ExecutorService cacheWarmupExecutor;
/**
* 初始化方法 - 创建线程池
*/
@PostConstruct
public void init() {
// 创建固定大小的线程池用于缓存预热
cacheWarmupExecutor = Executors.newFixedThreadPool(3);
logger.info("缓存预热线程池初始化完成");
}
/**
* 缓存预热 - 应用启动时执行
*/
@PostConstruct
@Async
public void warmUpCacheOnStartup() {
try {
// 延迟启动,等待应用完全启动
Thread.sleep(10000);
logger.info("开始执行应用启动缓存预热...");
// 预热热门店铺数据
warmUpPopularShops();
// 预热按类型分类的店铺
warmUpShopsByType();
// 预热地理位置数据
warmUpGeoData();
logger.info("应用启动缓存预热完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("缓存预热被中断", e);
} catch (Exception e) {
logger.error("缓存预热执行失败", e);
}
}
/**
* 预热热门店铺数据
*/
private void warmUpPopularShops() {
cacheWarmupExecutor.submit(() -> {
try {
logger.info("开始预热热门店铺数据...");
// 查询热门店铺(这里可以根据业务逻辑调整,比如按评分、销量等)
List<Shop> popularShops = this.query()
.orderByDesc("score") // 假设有评分字段
.last("LIMIT 50") // 预热前50个热门店铺
.list();
int successCount = 0;
for (Shop shop : popularShops) {
try {
// 使用queryById方法,会自动缓存
this.queryById(shop.getId());
successCount++;
// 批量操作时稍微延迟,避免对数据库造成压力
Thread.sleep(10);
} catch (Exception e) {
logger.warn("预热店铺 {} 失败: {}", shop.getId(), e.getMessage());
}
}
logger.info("热门店铺数据预热完成,成功预热 {} 个店铺", successCount);
} catch (Exception e) {
logger.error("预热热门店铺数据失败", e);
}
});
}
/**
* 预热按类型分类的店铺
*/
private void warmUpShopsByType() {
cacheWarmupExecutor.submit(() -> {
try {
logger.info("开始预热按类型分类的店铺数据...");
// 使用正确的 QueryWrapper 创建方式
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Shop> queryWrapper =
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
queryWrapper.select("DISTINCT type_id").isNotNull("type_id");
// 获取所有店铺类型
List<Object> typeList = this.baseMapper.selectObjs(queryWrapper);
int typeCount = 0;
for (Object typeObj : typeList) {
if (typeObj != null) {
try {
// 根据Shop实体中typeId的实际类型进行转换
Integer typeId;
if (typeObj instanceof Long) {
typeId = ((Long) typeObj).intValue();
} else if (typeObj instanceof Integer) {
typeId = (Integer) typeObj;
} else {
typeId = Integer.valueOf(typeObj.toString());
}
// 预热每种类型前几页数据
for (int current = 1; current <= 3; current++) {
this.queryShopByType(typeId, current, null, null);
}
typeCount++;
Thread.sleep(50); // 类型间延迟
} catch (Exception e) {
logger.warn("预热类型 {} 的店铺失败: {}", typeObj, e.getMessage());
}
}
}
logger.info("按类型分类的店铺数据预热完成,成功预热 {} 种类型", typeCount);
} catch (Exception e) {
logger.error("预热按类型分类的店铺数据失败", e);
}
});
}
/**
* 预热地理位置数据
*/
private void warmUpGeoData() {
cacheWarmupExecutor.submit(() -> {
try {
logger.info("开始预热地理位置数据...");
// 查询所有有地理坐标的店铺
List<Shop> shopsWithLocation = this.query()
.isNotNull("x")
.isNotNull("y")
.list();
// 按类型分组 - 修复类型问题
Map<Long, List<Shop>> shopsByType = new HashMap<>();
for (Shop shop : shopsWithLocation) {
if (shop.getTypeId() != null) {
// 使用Long作为key,因为Shop的typeId通常是Long类型
Long typeId = shop.getTypeId();
shopsByType.computeIfAbsent(typeId, k -> new ArrayList<>()).add(shop);
}
}
// 预热每种类型的地理位置数据
int geoCount = 0;
for (Map.Entry<Long, List<Shop>> entry : shopsByType.entrySet()) {
Long typeId = entry.getKey();
List<Shop> shops = entry.getValue();
try {
String key = SHOP_GEO_KEY + typeId;
// 批量添加地理位置数据
List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();
for (Shop shop : shops) {
if (shop.getX() != null && shop.getY() != null) {
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new org.springframework.data.geo.Point(shop.getX(), shop.getY())
));
}
}
if (!locations.isEmpty()) {
stringRedisTemplate.opsForGeo().add(key, locations);
geoCount++;
logger.debug("成功预热类型 {} 的地理位置数据,包含 {} 个店铺", typeId, locations.size());
}
Thread.sleep(30); // 类型间延迟
} catch (Exception e) {
logger.warn("预热类型 {} 的地理位置数据失败: {}", typeId, e.getMessage());
}
}
logger.info("地理位置数据预热完成,成功预热 {} 种类型的地理数据", geoCount);
} catch (Exception e) {
logger.error("预热地理位置数据失败", e);
}
});
}
/**
* 手动触发缓存预热
*/
@Override
public Result warmUpCache() {
logger.info("手动触发缓存预热...");
try {
// 异步执行缓存预热
warmUpPopularShops();
warmUpShopsByType();
warmUpGeoData();
return Result.ok("缓存预热任务已启动,请查看日志了解进度");
} catch (Exception e) {
logger.error("手动缓存预热失败", e);
return Result.fail("缓存预热失败: " + e.getMessage());
}
}
/**
* 预热指定店铺的缓存
*/
@Override
public Result warmUpShopCache(List<Long> shopIds) {
if (shopIds == null || shopIds.isEmpty()) {
return Result.fail("店铺ID列表不能为空");
}
cacheWarmupExecutor.submit(() -> {
try {
logger.info("开始预热指定店铺缓存,数量: {}", shopIds.size());
int successCount = 0;
for (Long shopId : shopIds) {
try {
this.queryById(shopId);
successCount++;
Thread.sleep(20); // 延迟避免压力过大
} catch (Exception e) {
logger.warn("预热店铺 {} 失败: {}", shopId, e.getMessage());
}
}
logger.info("指定店铺缓存预热完成,成功预热 {} 个店铺", successCount);
} catch (Exception e) {
logger.error("预热指定店铺缓存失败", e);
}
});
return Result.ok("指定店铺缓存预热任务已启动");
}
/**
* 获取缓存预热状态
*/
@Override
public Result getCacheWarmupStatus() {
try {
Cache shopCache = cacheManager.getCache("shopCache");
if (shopCache != null) {
Object nativeCache = shopCache.getNativeCache();
// 这里可以根据具体的缓存实现获取更详细的状态信息
Map<String, Object> status = new HashMap<>();
status.put("cacheType", nativeCache.getClass().getSimpleName());
status.put("executorActive", cacheWarmupExecutor != null && !cacheWarmupExecutor.isShutdown());
// 可以添加更多状态信息
logger.info("缓存预热状态查询: {}", status);
return Result.ok(status);
}
return Result.fail("缓存未就绪");
} catch (Exception e) {
logger.error("获取缓存预热状态失败", e);
return Result.fail("获取状态失败: " + e.getMessage());
}
}
@Override
@Cacheable(value = "shopCache", key = "#id", unless = "#result == null")
public Result queryById(Long id) {
logger.info("查询数据库获取店铺信息,店铺ID: {}", id);
// 直接查询数据库,缓存由注解自动处理
Shop shop = getById(id);
// 手动检查缓存状态(用于调试)
checkCacheStatus(id, shop);
if(shop == null){
logger.warn("店铺不存在,ID: {}", id);
return Result.fail("店铺不存在!");
}
logger.info("成功获取店铺信息,店铺名称: {}", shop.getName());
return Result.ok(shop);
}
@Override
@Transactional
@CacheEvict(value = "shopCache", key = "#shop.id")
public Result update(Shop shop) {
Long id = shop.getId();
if(id == null){
return Result.fail("店铺id不能为空");
}
logger.info("开始更新店铺信息,店铺ID: {}", id);
// 更新前先获取旧数据用于日志
Shop oldShop = getById(id);
if (oldShop != null) {
logger.info("更新前店铺名称: {}", oldShop.getName());
}
// 更新数据库
boolean success = updateById(shop);
if (success) {
logger.info("数据库更新成功,店铺ID: {}", id);
// 验证缓存是否已被清除
checkCacheAfterEvict(id);
} else {
logger.error("数据库更新失败,店铺ID: {}", id);
}
// 缓存由 @CacheEvict 注解自动删除
return Result.ok();
}
@CacheEvict(value = "shopCache", allEntries = true)
public Result clearShopCache() {
logger.info("清空所有店铺缓存");
// 获取缓存统计信息
Cache shopCache = cacheManager.getCache("shopCache");
if (shopCache != null) {
logger.info("执行清空缓存操作");
}
return Result.ok("店铺缓存已清空");
}
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
logger.info("查询类型为 {} 的店铺,页码: {}, 坐标: ({}, {})", typeId, current, x, y);
// 1.判断是否需要根据坐标查询
if (x == null || y == null) {
// 不需要坐标查询,按数据库查询
logger.info("执行无坐标查询");
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
// 返回数据
logger.info("查询到 {} 条记录", page.getRecords().size());
return Result.ok(page.getRecords());
}
// 2.计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
logger.info("执行地理位置查询,从第 {} 条到第 {} 条", from, end);
// 3.查询redis、按照距离排序、分页。结果:shopId、distance
String key = SHOP_GEO_KEY + typeId;
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
.search(
key,
GeoReference.fromCoordinate(x, y),
new org.springframework.data.geo.Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
// 4.解析出id
if (results == null) {
logger.info("未找到附近店铺");
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
if (list.size() <= from) {
// 没有下一页了,结束
logger.info("没有更多店铺数据");
return Result.ok(Collections.emptyList());
}
// 4.1.截取 from ~ end的部分
List<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>(list.size());
list.stream().skip(from).forEach(result -> {
// 4.2.获取店铺id
String shopIdStr = result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
// 4.3.获取距离
Distance distance = result.getDistance();
distanceMap.put(shopIdStr, distance);
});
logger.info("找到 {} 个附近店铺,ID列表: {}", ids.size(), ids);
// 5.根据id查询Shop
String idStr = StrUtil.join(",", ids);
List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Shop shop : shops) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
logger.info("成功获取 {} 个店铺详细信息", shops.size());
// 6.返回
return Result.ok(shops);
}
/**
* 检查缓存状态(用于调试)
*/
private void checkCacheStatus(Long shopId, Shop shopFromDB) {
try {
Cache shopCache = cacheManager.getCache("shopCache");
if (shopCache != null) {
Cache.ValueWrapper cachedValue = shopCache.get(shopId);
if (cachedValue != null) {
Object value = cachedValue.get();
logger.info("缓存命中 - 店铺ID: {}, 缓存值: {}", shopId, value);
} else {
logger.info("缓存未命中 - 店铺ID: {}, 将从数据库查询", shopId);
// 如果是新查询的数据,应该会被自动缓存
if (shopFromDB != null) {
logger.info("数据库查询成功,数据将被缓存");
}
}
// 如果使用的是二级缓存,可以尝试获取原生缓存来检查两级缓存状态
Object nativeCache = shopCache.getNativeCache();
logger.debug("缓存原生类型: {}", nativeCache.getClass().getName());
} else {
logger.warn("未找到 shopCache 缓存实例");
}
} catch (Exception e) {
logger.error("检查缓存状态时发生错误", e);
}
}
/**
* 检查缓存清除后的状态
*/
private void checkCacheAfterEvict(Long shopId) {
try {
Cache shopCache = cacheManager.getCache("shopCache");
if (shopCache != null) {
Cache.ValueWrapper cachedValue = shopCache.get(shopId);
if (cachedValue == null) {
logger.info("缓存清除验证成功 - 店铺ID: {} 的缓存已被清除", shopId);
} else {
logger.warn("缓存清除验证失败 - 店铺ID: {} 的缓存仍然存在", shopId);
}
}
} catch (Exception e) {
logger.error("检查缓存清除状态时发生错误", e);
}
}
/**
* 手动获取缓存中的店铺信息(用于测试)
*/
public Result getCachedShop(Long id) {
try {
Cache shopCache = cacheManager.getCache("shopCache");
if (shopCache != null) {
Cache.ValueWrapper wrapper = shopCache.get(id);
if (wrapper != null) {
Object value = wrapper.get();
logger.info("手动查询缓存成功 - 店铺ID: {}, 值: {}", id, value);
return Result.ok(value);
} else {
logger.info("手动查询缓存未命中 - 店铺ID: {}", id);
return Result.fail("缓存中未找到该店铺");
}
} else {
return Result.fail("缓存管理器未就绪");
}
} catch (Exception e) {
logger.error("手动查询缓存时发生错误", e);
return Result.fail("查询缓存失败: " + e.getMessage());
}
}
/**
* Bean销毁时关闭线程池
*/
@javax.annotation.PreDestroy
public void destroy() {
if (cacheWarmupExecutor != null) {
try {
cacheWarmupExecutor.shutdown();
if (!cacheWarmupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cacheWarmupExecutor.shutdownNow();
}
logger.info("缓存预热线程池已关闭");
} catch (InterruptedException e) {
cacheWarmupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
小细节 这里要修改一下shop类,不然可能会报错,因为Java 8 时间类型在Redis序列化中的兼容性问题
package com.hmdp.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
import java.io.Serializable; import java.time.LocalDateTime;
/**
-
-
@author 虎哥
-
@since 2021-12-22 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("tb_shop") public class Shop implements Serializable {
private static final long serialVersionUID = 1L;
/**
- 主键 */ @TableId(value = "id", type = IdType.AUTO) private Long id;
/**
- 商铺名称 */ private String name;
/**
- 商铺类型的id */ private Long typeId;
/**
- 商铺图片,多个图片以','隔开 */ private String images;
/**
- 商圈,例如陆家嘴 */ private String area;
/**
- 地址 */ private String address;
/**
- 经度 */ private Double x;
/**
- 维度 */ private Double y;
/**
- 均价,取整数 */ private Long avgPrice;
/**
- 销量 */ private Integer sold;
/**
- 评论数量 */ private Integer comments;
/**
- 评分,1~5分,乘10保存,避免小数 */ private Integer score;
/**
- 营业时间,例如 10:00-22:00 */ private String openHours;
/**
- 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime createTime;
/**
- 更新时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime updateTime;
@TableField(exist = false) private Double distance; }
再加入一个RedisConfig类 package com.hmdp.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration public class RedisConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 注册 JavaTimeModule 来支持 Java 8 时间类型
objectMapper.registerModule(new JavaTimeModule());
// 禁用将日期序列化为时间戳
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用自定义的 ObjectMapper 创建序列化器
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(objectMapper());
// 设置序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
// 设置默认序列化器
redisTemplate.setDefaultSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
性能测试与结果 为验证二级缓存方案的实际效果,我们进行了严格的性能测试。 测试环境如下: 测试工具:JMeter 测试时长:每组15分钟 测试次数:6次 测试结果 test1:20920.3 test2: 20954.7 test3: 21000.6 test4:21049.2 test5:21133.6 test6:21103.7 平均QPS : 21027.0 测试的异常均为0.00%
二级缓存的优势与适用场景 优势 降低延迟:本地缓存查询时间通常在1ms以内,相比Redis网络请求(通常5-10ms)有显著优势。
减轻分布式缓存压力:减少对Redis的访问频率,使Redis能够处理更多其他请求。
提升系统稳定性:即使Redis服务暂时不可用,本地缓存仍能提供服务,避免系统雪崩。 适用场景 高并发读场景:如商品详情、用户信息等读多写少的场景
热点数据:访问频率极高的数据,如热门商品、热门店铺。
对延迟敏感的系统:如实时推荐、即时通讯等需要快速响应的系统。
结论 通过Spring Cache + Redis + Caffeine实现的二级缓存方案,有效解决了高并发场景下的性能瓶颈问题。测试结果表明,该方案能够稳定提供21k+的QPS(本地网络)