当前位置: 首页 > news >正文

Spring Boot + Redis 缓存性能优化实战:从5秒到毫秒级的性能提升

前言

在开发某调查系统时,我们遇到了严重的性能问题:GeoJSON数据接口响应时间长达5-6秒,数据量达到20MB+,用户体验极差。通过系统性的缓存优化,我们成功将响应时间从5秒优化到毫秒级,性能提升超过99%。

本文将详细介绍我们的优化过程,包括问题分析、解决方案、代码实现和性能测试结果。

1. 问题分析

1.1 性能瓶颈识别

通过浏览器开发者工具的网络面板,我们发现了以下性能问题:

user?_t=1757664118630     - 5.66s, 131KB
geojson?xzqdm=62&_t=...   - 2.15s, 20,602KB
left?xzqdm=62&_t=...      - 8ms, 11.3KB
right?xzqdm=62&_t=...     - 8ms, 7.6KB

关键发现:

  • _t 参数(时间戳)导致每次请求URL都不同
  • 相同数据重复查询,没有缓存机制
  • 大数据量接口(20MB+)每次都重新生成

1.2 根本原因分析

  1. 前端缓存破坏_t 参数使每个请求URL唯一,绕过浏览器缓存
  2. 后端缓存失效:Spring Cache的SpEL表达式处理_t参数时出现问题
  3. UserToken空值:未登录用户导致缓存键生成失败
  4. 缓存键冲突:多个接口使用相同的缓存名称

2. 解决方案设计

2.1 整体架构

我们采用了前后端双重缓存策略:

前端请求 → 前端缓存检查 → 后端Spring Cache → Redis → 数据库↓           ↓              ↓5MB限制    无大小限制      持久化存储

2.2 缓存策略

缓存层级存储位置大小限制过期时间用途
前端缓存localStorage5MB30分钟小数据快速响应
后端缓存Redis无限制1-24小时大数据持久化
数据库PostgreSQL/KingBase无限制永久数据源

3. 后端缓存优化实现

3.1 修复SpEL表达式问题

问题代码:

@Cacheable(value = "geojson", key = "#xzqdm ?: #userToken.user.gsddm")
public Result getGeoJsonList(String xzqdm, UserToken userToken) {// 当userToken为null时会抛出NullPointerException
}

修复后:

@Cacheable(value = "geojson", key = "'dd_' + (#xzqdm ?: (#userToken != null ? #userToken.user.gsddm : '62'))")
public Result getGeoJsonList(String xzqdm, UserToken userToken) {if (xzqdm == null) {xzqdm = (userToken != null && userToken.getUser() != null) ? userToken.getUser().getGsddm() : "62";}// 业务逻辑...
}

3.2 缓存键优化

为了避免不同接口的缓存冲突,我们为每个接口设计了独特的缓存键:

@Cacheable(value = "geojson", key = "'dd_' + (#xzqdm ?: (#userToken != null ? #userToken.user.gsddm : '62'))")@Cacheable(value = "geojson", key = "'env_' + (#xzqdm ?: (#userToken != null ? #userToken.user.gsddm : '62'))")@Cacheable(value = "geojson", key = "'prop_' + (#vo.xzqdm ?: (#userToken != null ? #userToken.user.gsddm : '62'))")// 统计面板
@Cacheable(value = "left_panel", key = "'left_' + (#xzqdm ?: (#userToken != null ? #userToken.user.gsddm : '62'))")
@Cacheable(value = "right_panel", key = "'right_' + (#xzqdm ?: (#userToken != null ? #userToken.user.gsddm : '62'))")

3.3 Redis缓存配置

@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {// 配置序列化器RedisSerializationContext.SerializationPair<String> stringPair = RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());RedisSerializationContext.SerializationPair<Object> objectPair = RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());// GeoJSON缓存配置 - 10分钟(数据变化频繁)RedisCacheConfiguration geojsonConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)).serializeKeysWith(stringPair).serializeValuesWith(objectPair).disableCachingNullValues();// 统计面板缓存配置 - 1小时RedisCacheConfiguration statsConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1)).serializeKeysWith(stringPair).serializeValuesWith(objectPair).disableCachingNullValues();return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30))).withCacheConfiguration("geojson", geojsonConfig).withCacheConfiguration("left_panel", statsConfig).withCacheConfiguration("right_panel", statsConfig).withCacheConfiguration("prop_panel", statsConfig).withCacheConfiguration("env_panel", statsConfig).withCacheConfiguration("user_auth", statsConfig).build();}
}

3.4 缓存管理服务

@Service
public class CacheService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 清理指定缓存*/public boolean clearCache(String cacheName) {try {Set<String> keys = redisTemplate.keys(cacheName + ":*");if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);return true;}return false;} catch (Exception e) {log.error("清理缓存失败: {}", e.getMessage());return false;}}/*** 清理区域相关缓存*/public int clearRegionCaches(String xzqdm) {int clearedCount = 0;String[] cacheNames = {"geojson", "left_panel", "right_panel", "prop_panel", "env_panel"};for (String cacheName : cacheNames) {if (clearCacheKey(cacheName, "dd_" + xzqdm) ||clearCacheKey(cacheName, "env_" + xzqdm) ||clearCacheKey(cacheName, "prop_" + xzqdm) ||clearCacheKey(cacheName, "left_" + xzqdm) ||clearCacheKey(cacheName, "right_" + xzqdm)) {clearedCount++;}}return clearedCount;}
}

4. 前端缓存优化实现

4.1 缓存工具类

// geojson-cache.ts
interface CacheItem {data: any;timestamp: number;version: string;
}class GeoJsonCache {private readonly CACHE_VERSION = 'v1.0';private readonly EXPIRE_TIME = 30 * 60 * 1000; // 30分钟private readonly MAX_SIZE_MB = 5; // 5MB限制private readonly MAX_FEATURES = 10000; // 最大要素数量/*** 获取缓存的GeoJSON数据*/async getCachedDcydGeoJson(params: any, forceRefresh = false): Promise<any> {const cacheKey = this.generateCacheKey('dcdy', params);if (!forceRefresh) {const cached = this.getFromCache(cacheKey);if (cached) {console.log('使用缓存的 GeoJSON 数据:', cacheKey);return cached;}}console.log('从 API 获取 GeoJSON 数据:', params);const data = await this.fetchFromAPI('/api/geojson', params);// 检查数据大小if (this.isDataTooLarge(data)) {console.warn('数据过大, 跳过缓存存储');return data;}this.saveToCache(cacheKey, data);return data;}/*** 检查数据是否过大*/private isDataTooLarge(data: any): boolean {const jsonString = JSON.stringify(data);const sizeMB = new Blob([jsonString]).size / (1024 * 1024);if (sizeMB > this.MAX_SIZE_MB) {console.warn(`数据过大 (${sizeMB.toFixed(2)}MB), 超过限制 (${this.MAX_SIZE_MB}MB)`);return true;}if (data.features && data.features.length > this.MAX_FEATURES) {console.warn(`要素数量过多 (${data.features.length}), 超过限制 (${this.MAX_FEATURES})`);return true;}return false;}/*** 生成缓存键*/private generateCacheKey(type: string, params: any): string {const paramStr = params ? Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&') : 'default';return `geojson_cache_${this.CACHE_VERSION}_${type}_${paramStr}`;}/*** 从缓存获取数据*/private getFromCache(key: string): any {try {const cached = localStorage.getItem(key);if (!cached) return null;const item: CacheItem = JSON.parse(cached);// 检查版本if (item.version !== this.CACHE_VERSION) {localStorage.removeItem(key);return null;}// 检查过期时间if (Date.now() - item.timestamp > this.EXPIRE_TIME) {localStorage.removeItem(key);return null;}return item.data;} catch (error) {console.error('读取缓存失败:', error);return null;}}/*** 保存数据到缓存*/private saveToCache(key: string, data: any): void {try {const item: CacheItem = {data,timestamp: Date.now(),version: this.CACHE_VERSION};localStorage.setItem(key, JSON.stringify(item));console.log('GeoJSON 数据已缓存:', key);} catch (error) {if (error.name === 'QuotaExceededError') {console.warn('存储空间不足,清理旧缓存');this.cleanOldCache();// 重试一次try {localStorage.setItem(key, JSON.stringify(item));} catch (retryError) {console.warn('重试后仍然失败,跳过缓存');}} else {console.error('保存缓存失败:', error);}}}/*** 清理旧缓存*/private cleanOldCache(): void {const keys = Object.keys(localStorage).filter(key => key.startsWith('geojson_cache_')).map(key => ({key,timestamp: this.getCacheTimestamp(key)})).sort((a, b) => a.timestamp - b.timestamp);// 删除最旧的30%const deleteCount = Math.ceil(keys.length * 0.3);for (let i = 0; i < deleteCount; i++) {localStorage.removeItem(keys[i].key);}}
}export const geoJsonCache = new GeoJsonCache();

4.2 在组件中使用

<template><div><SamplePointsMap :geoJsonData="geoJsonData" /><a-button @click="refreshData" :loading="loading">刷新数据</a-button></div>
</template><script setup>
import { ref, onMounted } from 'vue'
import { geoJsonCache } from '@/utils/cache/geojson-cache'const geoJsonData = ref(null)
const loading = ref(false)const fetchData = async (forceRefresh = false) => {loading.value = truetry {const data = await geoJsonCache.getCachedDcydGeoJson({ xzqdm: '420000' }, forceRefresh)geoJsonData.value = data} catch (error) {console.error('获取数据失败:', error)} finally {loading.value = false}
}const refreshData = () => {fetchData(true) // 强制刷新
}onMounted(() => {fetchData() // 首次加载
})
</script>

5. 性能测试结果

5.1 优化前后对比

接口优化前优化后提升幅度
user接口5.66s15ms99.7%
geojson接口2.15s8ms99.6%
left面板8ms3ms62.5%
right面板8ms2ms75%

5.2 缓存命中率

首次请求: 0% 命中率(建立缓存)
第二次请求: 100% 命中率(从缓存获取)
第三次请求: 100% 命中率(从缓存获取)

5.3 内存使用情况

  • Redis内存使用: 约200MB(存储所有缓存数据)
  • 前端localStorage: 约3MB(小数据缓存)
  • 数据库查询减少: 90%以上

6. 监控和调试

6.1 缓存统计接口

@RestController
@RequestMapping("/cache")
public class CacheController {@Autowiredprivate CacheService cacheService;@GetMapping("/stats")public Result getCacheStats() {Map<String, Object> stats = new HashMap<>();stats.put("totalKeys", cacheService.getTotalCacheKeys());stats.put("memoryUsage", cacheService.getMemoryUsage());stats.put("hitRate", cacheService.getHitRate());return Result.OK(stats);}@PostMapping("/clear")public Result clearCache(@RequestParam String cacheType) {boolean success = cacheService.clearCache(cacheType);return Result.OK(success ? "清理成功" : "清理失败");}
}

6.2 性能监控

@Component
public class CachePerformanceMonitor {private final MeterRegistry meterRegistry;public CachePerformanceMonitor(MeterRegistry meterRegistry) {this.meterRegistry = meterRegistry;}@EventListenerpublic void handleCacheHit(CacheHitEvent event) {meterRegistry.counter("cache.hit", "name", event.getCacheName()).increment();}@EventListenerpublic void handleCacheMiss(CacheMissEvent event) {meterRegistry.counter("cache.miss", "name", event.getCacheName()).increment();}
}

7. 最佳实践总结

7.1 缓存设计原则

  1. 缓存键设计:使用有意义的前缀,避免冲突
  2. 过期时间设置:根据数据更新频率合理设置
  3. 空值处理:避免缓存null值,浪费存储空间
  4. 版本控制:支持缓存版本管理,便于升级

7.2 性能优化技巧

  1. 批量操作:使用Redis Pipeline减少网络开销
  2. 压缩存储:大数据使用压缩算法减少存储空间
  3. 预热缓存:系统启动时预加载热点数据
  4. 监控告警:设置缓存命中率告警,及时发现问题

7.3 常见问题解决

  1. 缓存穿透:使用布隆过滤器或缓存空值
  2. 缓存雪崩:设置随机过期时间,避免同时失效
  3. 缓存击穿:使用分布式锁,避免热点数据重建
  4. 内存溢出:设置合理的过期时间和清理策略

8. 总结

通过系统性的缓存优化,我们成功解决了土壤调查系统的性能问题:

  • 响应时间:从5秒优化到毫秒级
  • 用户体验:大幅提升,页面加载流畅
  • 系统稳定性:减少数据库压力,提高并发能力
  • 开发效率:提供完整的缓存管理工具

缓存优化是一个持续的过程,需要根据业务特点和数据变化规律不断调整策略。希望本文的经验能够帮助到有类似需求的开发者。


文章转载自:

http://v1xayF24.pnLjy.cn
http://9gvS5ODZ.pnLjy.cn
http://aXX76tao.pnLjy.cn
http://OqR2IzKg.pnLjy.cn
http://bURdrC9P.pnLjy.cn
http://Bk054Zpk.pnLjy.cn
http://nS27dkEM.pnLjy.cn
http://QtqI6XQB.pnLjy.cn
http://MyVd1K9b.pnLjy.cn
http://FFRxBwYP.pnLjy.cn
http://SYyzzSNF.pnLjy.cn
http://uOvxJ4Wa.pnLjy.cn
http://U9kO0GMw.pnLjy.cn
http://cSCFcgFX.pnLjy.cn
http://HU7JwJJo.pnLjy.cn
http://8vhjKCzy.pnLjy.cn
http://9goyI72T.pnLjy.cn
http://HFvFqnXF.pnLjy.cn
http://DQ8T5bLA.pnLjy.cn
http://pwUkqz8V.pnLjy.cn
http://ZdaUJNfq.pnLjy.cn
http://6c4JXX3N.pnLjy.cn
http://0AWf53mb.pnLjy.cn
http://dKCPYNDC.pnLjy.cn
http://cmmMOkND.pnLjy.cn
http://cm16KCMm.pnLjy.cn
http://FAdA37T3.pnLjy.cn
http://lPFaSlgZ.pnLjy.cn
http://Ewmqek3e.pnLjy.cn
http://9THm98ye.pnLjy.cn
http://www.dtcms.com/a/379999.html

相关文章:

  • 【Vue2手录09】购物车实战
  • 【论文阅读】Uncertainty Modeling for Out-of-Distribution Generalization (ICLR 2022)
  • PAT乙级_1111 对称日_Python_AC解法_无疑难点
  • Kafka面试精讲 Day 16:生产者性能优化策略
  • vue 批量自动引入并注册组件或路由
  • Kubernetes(K8s)详解
  • 趣味学solana(介绍)
  • Apache Thrift:跨语言服务开发的高性能RPC框架指南
  • Flutter 应用国际化 (i18n) 与本地化 (l10n) 完整指南
  • 第 5 篇:深入浅出学 Java 语言(JDK8 版)—— 精通类与对象进阶,掌握 Java 面向对象核心能力
  • Gin-Vue-Admin学习笔记
  • Golang關於信件的
  • The 2024 ICPC Asia East Continent Online Contest (I)
  • 【数所有因子和快速新解/范围亲密数/分解因式怎么去掉重复项】2022-10-31
  • SQL语句执行时间太慢,有什么优化措施?以及衍生的相关问题
  • 【论文阅读】Language-Guided Image Tokenization for Generation
  • PHP:从入门到实战的全方位指南
  • 经典动态规划题解
  • 商城购物系统自动化测试报告
  • [工作表控件20] 拼音排序功能:中文数据高效检索实战指南
  • 9120 部 TMDb 高分电影数据集 | 7 列全维度指标 (评分 / 热度 / 剧情)+API 权威源 | 电影趋势分析 / 推荐系统 / NLP 建模用
  • 【Java】多态
  • LeetCode热题 438.找到字符中所有字母异位词 (滑动窗口)
  • 解决 N1 ARMBIAN Prometheus 服务启动失败问题
  • Linux 正则表达式详解(基础 + 扩展 + 实操)
  • 01.【Linux系统编程】Linux初识(Linux内核版本、基础指令、理论知识、shell命令及运行原理)
  • MATLAB 的无人机 PID 控制及智能 PID 控制器设计的仿真
  • D007 django+neo4j三维知识图谱医疗问答系统|3D+2D双知识图谱可视化+问答+寻医问药系统
  • 5G单兵图传 5G单兵 单兵图传 无线图传 无线图传方案 无人机图传解决方案 指挥中心大屏一目了然
  • npm / yarn / pnpm 包管理器对比与最佳实践(含国内镜像源配置与缓存优化)