黑马点评优化:基于Spring Cache的二级缓存实现与性能测试

1 阅读15分钟

引言: 在高并发场景下,缓存系统已成为提升系统性能的关键手段。然而,单一的分布式缓存方案(如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(本地网络)