内江市网站建设_网站建设公司_模板建站_seo优化
2025/12/18 1:40:55 网站建设 项目流程

这些年我参与设计过很多系统,越来越深刻地认识到:一个系统的性能如何,很大程度上取决于缓存用得怎么样

同样是缓存,为何有人用起来系统飞升,有人却踩坑不断?

有些小伙伴在工作中可能遇到过这样的困惑:知道要用缓存,但面对本地缓存、Redis、Memcached、CDN等多种选择,到底该用哪种?

今天这篇文章跟大家一起聊聊工作中最常用的6种缓存,希望对你会有所帮助。

01 为什么缓存如此重要?

在正式介绍各种缓存之前,我们先要明白:为什么要用缓存?

想象这样一个场景:你的电商网站首页,每次打开都要从数据库中查询轮播图、热门商品、分类信息等数据。

如果每秒有1万个用户访问,数据库就要承受1万次查询压力。

// 没有缓存时的查询 public Product getProductById(Long id) { // 每次都直接查询数据库 return productDao.findById(id); // 每次都是慢速的磁盘IO }

这就是典型的无缓存场景

数据库的磁盘IO速度远低于内存,当并发量上来后,系统响应变慢,数据库连接池被占满,最终导致服务不可用。

缓存的核心价值可以用下面这个公式理解:

系统性能 = (缓存命中率 × 缓存访问速度) + ((1 - 缓存命中率) × 后端访问速度)

缓存之所以能提升性能,基于两个计算机科学的基本原理:

  1. 局部性原理:程序访问的数据通常具有时间和空间局部性

  2. 存储层次结构:不同存储介质的速度差异巨大(内存比SSD快100倍,比HDD快10万倍)

从用户请求到数据返回,数据可能经过的各级缓存路径如下图所示:

理解了缓存的重要性,接下来我们逐一剖析这六种最常用的缓存技术。

02 本地缓存:最简单直接的性能提升

本地缓存指的是在应用进程内部维护的缓存存储,数据存储在JVM堆内存中。

核心特点

  • 访问最快:直接内存操作,无网络开销

  • 实现简单:无需搭建额外服务

  • 数据隔离:每个应用实例独享自己的缓存

常用实现

1. Guava Cache:Google提供的优秀本地缓存库

// Guava Cache 示例 LoadingCache<Long, Product> productCache = CacheBuilder.newBuilder() .maximumSize(10000) // 最大缓存项数 .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期 .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期 .recordStats() // 开启统计 .build(new CacheLoader<Long, Product>() { @Override public Product load(Long productId) { // 当缓存未命中时,自动加载数据 return productDao.findById(productId); } }); // 使用缓存 public Product getProduct(Long id) { try { return productCache.get(id); } catch (ExecutionException e) { thrownew RuntimeException("加载产品失败", e); } }

2. Caffeine:Guava Cache的现代替代品,性能更优

// Caffeine 示例(性能优于Guava Cache) Cache<Long, Product> caffeineCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .refreshAfterWrite(1, TimeUnit.MINUTES) // 支持刷新,Guava不支持 .recordStats() .build(productId -> productDao.findById(productId)); // 异步获取 public CompletableFuture<Product> getProductAsync(Long id) { return caffeineCache.get(id, productId -> CompletableFuture.supplyAsync(() -> productDao.findById(productId))); }

适用场景

  • 数据量不大(通常不超过10万条)

  • 数据变化不频繁

  • 对访问速度要求极致

  • 如:配置信息、静态字典、用户会话信息(短期)

优缺点分析

  • 优点:极速访问、零网络开销、实现简单

  • 缺点:数据不一致(各节点独立)、内存限制、重启丢失

有些小伙伴在工作中可能会犯一个错误:在分布式系统中过度依赖本地缓存,导致各节点数据不一致。记住:本地缓存适合存储只读或弱一致性的数据

03 分布式缓存之王:Redis的深度解析

当数据需要在多个应用实例间共享时,本地缓存就不够用了,这时需要分布式缓存。而Redis无疑是这一领域的王者。

Redis的核心优势

// Spring Boot + Redis 示例 @Component publicclass ProductCacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; privatestaticfinal String PRODUCT_KEY_PREFIX = "product:"; privatestaticfinal Duration CACHE_TTL = Duration.ofMinutes(30); // 缓存查询 public Product getProduct(Long id) { String key = PRODUCT_KEY_PREFIX + id; // 1. 先查缓存 Product product = (Product) redisTemplate.opsForValue().get(key); if (product != null) { return product; } // 2. 缓存未命中,查数据库 product = productDao.findById(id); if (product != null) { // 3. 写入缓存 redisTemplate.opsForValue().set(key, product, CACHE_TTL); } return product; } // 使用更高效的方式:缓存空值防止缓存穿透 public Product getProductWithNullCache(Long id) { String key = PRODUCT_KEY_PREFIX + id; String nullKey = PRODUCT_KEY_PREFIX + "null:" + id; // 检查是否是空值(防缓存穿透) if (Boolean.TRUE.equals(redisTemplate.hasKey(nullKey))) { returnnull; } Product product = (Product) redisTemplate.opsForValue().get(key); if (product != null) { return product; } product = productDao.findById(id); if (product == null) { // 缓存空值,短时间过期 redisTemplate.opsForValue().set(nullKey, "", Duration.ofMinutes(5)); returnnull; } redisTemplate.opsForValue().set(key, product, CACHE_TTL); return product; } }

Redis的丰富数据结构

Redis不只是简单的Key-Value存储,它的多种数据结构适应不同场景:

数据结构

适用场景

示例

String

缓存对象、计数器

SET user:1 '{"name":"张三"}'

Hash

存储对象属性

HSET product:1001 name "手机" price 2999

List

消息队列、最新列表

LPUSH news:latest "新闻标题"

Set

标签、共同好友

SADD user:100:tags "数码" "科技"

Sorted Set

排行榜、延迟队列

ZADD leaderboard 95 "玩家A"

Bitmap

用户签到、活跃统计

SETBIT sign:2023:10 1 1

集群模式选择

适用场景

  • 会话存储(分布式Session)

  • 排行榜、计数器

  • 消息队列

  • 分布式锁

  • 热点数据缓存

有些小伙伴在工作中使用Redis时,只把它当简单的Key-Value用,这就像用瑞士军刀只开瓶盖一样浪费。

深入理解Redis的数据结构,能让你的系统设计更优雅高效

04 Memcached:简单高效的分布式缓存

在Redis崛起之前,Memcached是分布式缓存的首选。

虽然现在Redis更流行,但Memcached在某些场景下仍有其价值。

Memcached vs Redis 核心区别

// Memcached 客户端示例(使用XMemcached) publicclass MemcachedService { private MemcachedClient memcachedClient; public void init() throws IOException { // 创建客户端 memcachedClient = new XMemcachedClientBuilder( AddrUtil.getAddresses("server1:11211 server2:11211")) .build(); } public Product getProduct(Long id) throws Exception { String key = "product_" + id; // 从Memcached获取 Product product = memcachedClient.get(key); if (product != null) { return product; } // 缓存未命中 product = productDao.findById(id); if (product != null) { // 存储到Memcached,过期时间30分钟 memcachedClient.set(key, 30 * 60, product); } return product; } }

两者的核心差异对比

特性

Redis

Memcached

数据结构

丰富(String、Hash、List等)

简单(Key-Value)

持久化

支持(RDB/AOF)

不支持

线程模型

单线程

多线程

内存管理

多种策略,可持久化

纯内存,重启丢失

使用场景

缓存+多样化数据结构

纯缓存

何时选择Memcached?

  1. 纯缓存场景:只需要简单的Key-Value缓存

  2. 超大Value存储:Memcached对超大Value支持更好

  3. 多线程高并发:Memcached的多线程模型在极端并发下可能表现更好

05 CDN缓存:加速静态资源的利器

有些小伙伴可能会疑惑:CDN也算缓存吗?当然算,而且是地理位置最近的缓存。

CDN的工作原理

CDN(Content Delivery Network)通过在各地部署边缘节点,将静态资源缓存到离用户最近的节点。

// 在应用中生成CDN链接 publicclass CDNService { private String cdnDomain = "https://cdn.yourcompany.com"; public String getCDNUrl(String relativePath) { // 添加版本号或时间戳,防止缓存旧版本 String version = getFileVersion(relativePath); return String.format("%s/%s?v=%s", cdnDomain, relativePath, version); } // 上传文件到CDN的示例(伪代码) public void uploadToCDN(File file, String remotePath) { // 1. 上传到源站 uploadToOrigin(file, remotePath); // 2. 触发CDN预热(将文件主动推送到边缘节点) preheatCDN(remotePath); // 3. 刷新旧缓存(如果需要) refreshCDNCache(remotePath); } }

CDN缓存策略配置

# Nginx中的CDN缓存配置示例 location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 365d; # 缓存一年 add_header Cache-Control "public, immutable"; # 添加版本号作为查询参数 if ($query_string ~* "^v=\d+") { expires max; } }

适用场景

  • 静态资源:图片、CSS、JS文件

  • 软件下载包

  • 视频流媒体

  • 全球访问的网站

06 浏览器缓存:最前端的性能优化

浏览器缓存是最容易被忽视但效果最直接的缓存层级。合理利用浏览器缓存,可以大幅减少服务器压力。

HTTP缓存头详解

// Spring Boot中设置HTTP缓存头 @RestController publicclass ResourceController { @GetMapping("/static/{filename}") public ResponseEntity<Resource> getStaticFile(@PathVariable String filename) { Resource resource = loadResource(filename); return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)) // 缓存7天 .eTag(computeETag(resource)) // ETag用于协商缓存 .lastModified(resource.lastModified()) // 最后修改时间 .body(resource); } @GetMapping("/dynamic/data") public ResponseEntity<Object> getDynamicData() { Object data = getData(); // 动态数据设置较短缓存 return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS)) // 30秒 .body(data); } }

浏览器缓存的两种类型

最佳实践

  1. 静态资源:设置长时间缓存(如一年),通过文件名哈希处理更新

  2. 动态数据:根据业务需求设置合理缓存时间

  3. API响应:适当使用ETag和Last-Modified

07 数据库缓存:容易被忽略的内部优化

数据库自身也有缓存机制,理解这些机制能帮助我们写出更高效的SQL。

MySQL查询缓存(已废弃但值得了解)

-- 查看查询缓存状态(MySQL 5.7及之前) SHOW VARIABLES LIKE 'query_cache%'; -- 在8.0之前,可以通过以下方式利用查询缓存 SELECT SQL_CACHE * FROM products WHERE category_id = 10;

InnoDB缓冲池(Buffer Pool)

这是MySQL性能的关键,缓存的是数据页和索引页

-- 查看缓冲池状态 SHOW ENGINE INNODB STATUS; -- 重要的监控指标 -- 缓冲池命中率 = (1 - (innodb_buffer_pool_reads / innodb_buffer_pool_read_requests)) * 100% -- 命中率应尽可能接近100%

数据库级缓存最佳实践

  1. 合理设置缓冲池大小:通常是系统内存的50%-70%

  2. 优化查询:避免全表扫描,合理使用索引

  3. 预热缓存:重启后主动加载热点数据

  4. 监控命中率:持续优化

有些小伙伴可能会过度依赖应用层缓存,而忽略了数据库自身的缓存优化。

数据库缓存是最后一道防线,优化好它能让整个系统更健壮

08 综合对比与选型指南

接下来,我给大家一个选型指南:

实战中的多级缓存架构

在实际的高并发系统中,我们往往会采用多级缓存策略

// 多级缓存示例:本地缓存 + Redis @Component publicclass MultiLevelCacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; // 一级缓存:本地缓存 private Cache<Long, Product> localCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(30, TimeUnit.SECONDS) // 本地缓存时间短 .build(); // 二级缓存:Redis privatestaticfinal Duration REDIS_TTL = Duration.ofMinutes(10); public Product getProductWithMultiCache(Long id) { // 1. 查本地缓存 Product product = localCache.getIfPresent(id); if (product != null) { return product; } // 2. 查Redis String redisKey = "product:" + id; product = (Product) redisTemplate.opsForValue().get(redisKey); if (product != null) { // 回填本地缓存 localCache.put(id, product); return product; } // 3. 查数据库 product = productDao.findById(id); if (product != null) { // 写入Redis redisTemplate.opsForValue().set(redisKey, product, REDIS_TTL); // 写入本地缓存 localCache.put(id, product); } return product; } }

09 缓存常见问题与解决方案

在使用缓存的过程中,我们不可避免地会遇到一些问题:

1. 缓存穿透

问题:大量请求查询不存在的数据,绕过缓存直接击穿数据库。

解决方案

// 缓存空值方案 public Product getProductSafe(Long id) { String key = "product:" + id; String nullKey = "product:null:" + id; // 检查空值标记 if (redisTemplate.hasKey(nullKey)) { returnnull; } Product product = (Product) redisTemplate.opsForValue().get(key); if (product != null) { return product; } product = productDao.findById(id); if (product == null) { // 缓存空值,短时间过期 redisTemplate.opsForValue().set(nullKey, "", Duration.ofMinutes(5)); returnnull; } redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30)); return product; }

2. 缓存雪崩

问题:大量缓存同时过期,请求全部打到数据库。

解决方案

// 差异化过期时间 private Duration getRandomTTL() { // 基础30分钟 + 随机0-10分钟 long baseMinutes = 30; long randomMinutes = ThreadLocalRandom.current().nextLong(0, 10); return Duration.ofMinutes(baseMinutes + randomMinutes); }

3. 缓存击穿

问题:热点Key过期瞬间,大量并发请求同时查询数据库。

解决方案

// 使用互斥锁(分布式锁) public Product getProductWithLock(Long id) { String key = "product:" + id; Product product = (Product) redisTemplate.opsForValue().get(key); if (product == null) { // 尝试获取分布式锁 String lockKey = "lock:product:" + id; boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (locked) { try { // 双重检查 product = (Product) redisTemplate.opsForValue().get(key); if (product == null) { product = productDao.findById(id); if (product != null) { redisTemplate.opsForValue() .set(key, product, Duration.ofMinutes(30)); } } } finally { // 释放锁 redisTemplate.delete(lockKey); } } else { // 未获取到锁,等待后重试 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getProductWithLock(id); // 递归重试 } } return product; }

10 总结

通过这篇文章,我们系统地探讨了工作中最常用的六种缓存技术。

每种缓存都有其独特的价值和应用场景:

  1. 本地缓存:适合进程内、变化不频繁的只读数据

  2. Redis:功能丰富的分布式缓存,适合大多数共享缓存场景

  3. Memcached:简单高效的分布式缓存,适合纯Key-Value场景

  4. CDN缓存:加速静态资源,提升全球访问速度

  5. 浏览器缓存:最前端的优化,减少不必要的网络请求

  6. 数据库缓存:最后一道防线,优化数据库访问性能

缓存使用的核心原则可以总结为以下几点

  • 分级缓存:合理利用多级缓存架构

  • 合适粒度:根据业务特点选择缓存粒度

  • 及时更新:设计合理的缓存更新策略

  • 监控告警:建立完善的缓存监控体系

有些小伙伴在工作中使用缓存时,容易陷入两个极端:要么过度设计,所有数据都加缓存;要么忽视缓存,让数据库承受所有压力。

我们需要懂得在合适的地方使用合适的缓存,在性能和复杂性之间找到最佳平衡点

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询