大家好,我是苏三,又跟大家见面了前言这些年我参与设计过很多系统,越来越深刻地认识到:一个系统的性能如何,很大程度上取决于缓存用得怎么样。

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

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

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

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

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

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

代码语言:javascript复制// 没有缓存时的查询

public Product getProductById(Long id) {

// 每次都直接查询数据库

return productDao.findById(id); // 每次都是慢速的磁盘IO

}

这就是典型的无缓存场景。

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

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

代码语言:javascript复制系统性能 = (缓存命中率 × 缓存访问速度) + ((1 - 缓存命中率) × 后端访问速度)

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

局部性原理:程序访问的数据通常具有时间和空间局部性存储层次结构:不同存储介质的速度差异巨大(内存比SSD快100倍,比HDD快10万倍)从用户请求到数据返回,数据可能经过的各级缓存路径如下图所示:

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

02 本地缓存:最简单直接的性能提升本地缓存指的是在应用进程内部维护的缓存存储,数据存储在JVM堆内存中。

核心特点访问最快:直接内存操作,无网络开销实现简单:无需搭建额外服务数据隔离:每个应用实例独享自己的缓存常用实现1. Guava Cache:Google提供的优秀本地缓存库

代码语言:javascript复制// Guava Cache 示例

LoadingCache productCache = CacheBuilder.newBuilder()

.maximumSize(10000) // 最大缓存项数

.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期

.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期

.recordStats() // 开启统计

.build(new CacheLoader() {

@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的现代替代品,性能更优

代码语言:javascript复制// Caffeine 示例(性能优于Guava Cache)

Cache caffeineCache = Caffeine.newBuilder()

.maximumSize(10_000)

.expireAfterWrite(10, TimeUnit.MINUTES)

.refreshAfterWrite(1, TimeUnit.MINUTES) // 支持刷新,Guava不支持

.recordStats()

.build(productId -> productDao.findById(productId));

// 异步获取

public CompletableFuture getProductAsync(Long id) {

return caffeineCache.get(id, productId ->

CompletableFuture.supplyAsync(() -> productDao.findById(productId)));

}

适用场景数据量不大(通常不超过10万条)数据变化不频繁对访问速度要求极致如:配置信息、静态字典、用户会话信息(短期)优缺点分析优点:极速访问、零网络开销、实现简单缺点:数据不一致(各节点独立)、内存限制、重启丢失有些小伙伴在工作中可能会犯一个错误:在分布式系统中过度依赖本地缓存,导致各节点数据不一致。记住:本地缓存适合存储只读或弱一致性的数据。

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

Redis的核心优势代码语言:javascript复制// Spring Boot + Redis 示例

@Component

publicclass ProductCacheService {

@Autowired

private RedisTemplate 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 核心区别代码语言:javascript复制// 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?纯缓存场景:只需要简单的Key-Value缓存超大Value存储:Memcached对超大Value支持更好多线程高并发:Memcached的多线程模型在极端并发下可能表现更好05 CDN缓存:加速静态资源的利器有些小伙伴可能会疑惑:CDN也算缓存吗?当然算,而且是地理位置最近的缓存。

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

代码语言:javascript复制// 在应用中生成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缓存策略配置代码语言:javascript复制# 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缓存头详解代码语言:javascript复制// Spring Boot中设置HTTP缓存头

@RestController

publicclass ResourceController {

@GetMapping("/static/{filename}")

public ResponseEntity 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 getDynamicData() {

Object data = getData();

// 动态数据设置较短缓存

return ResponseEntity.ok()

.cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS)) // 30秒

.body(data);

}

}

浏览器缓存的两种类型最佳实践静态资源:设置长时间缓存(如一年),通过文件名哈希处理更新动态数据:根据业务需求设置合理缓存时间API响应:适当使用ETag和Last-Modified07 数据库缓存:容易被忽略的内部优化数据库自身也有缓存机制,理解这些机制能帮助我们写出更高效的SQL。

MySQL查询缓存(已废弃但值得了解)代码语言:javascript复制-- 查看查询缓存状态(MySQL 5.7及之前)

SHOW VARIABLES LIKE 'query_cache%';

-- 在8.0之前,可以通过以下方式利用查询缓存

SELECT SQL_CACHE * FROM products WHERE category_id = 10;

InnoDB缓冲池(Buffer Pool)这是MySQL性能的关键,缓存的是数据页和索引页。

代码语言:javascript复制-- 查看缓冲池状态

SHOW ENGINE INNODB STATUS;

-- 重要的监控指标

-- 缓冲池命中率 = (1 - (innodb_buffer_pool_reads / innodb_buffer_pool_read_requests)) * 100%

-- 命中率应尽可能接近100%

数据库级缓存最佳实践合理设置缓冲池大小:通常是系统内存的50%-70%优化查询:避免全表扫描,合理使用索引预热缓存:重启后主动加载热点数据监控命中率:持续优化有些小伙伴可能会过度依赖应用层缓存,而忽略了数据库自身的缓存优化。

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

08 综合对比与选型指南接下来,我给大家一个选型指南:

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

代码语言:javascript复制// 多级缓存示例:本地缓存 + Redis

@Component

publicclass MultiLevelCacheService {

@Autowired

private RedisTemplate redisTemplate;

// 一级缓存:本地缓存

private Cache 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. 缓存穿透问题:大量请求查询不存在的数据,绕过缓存直接击穿数据库。

解决方案:

代码语言:javascript复制// 缓存空值方案

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. 缓存雪崩问题:大量缓存同时过期,请求全部打到数据库。

解决方案:

代码语言:javascript复制// 差异化过期时间

private Duration getRandomTTL() {

// 基础30分钟 + 随机0-10分钟

long baseMinutes = 30;

long randomMinutes = ThreadLocalRandom.current().nextLong(0, 10);

return Duration.ofMinutes(baseMinutes + randomMinutes);

}

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

解决方案:

代码语言:javascript复制// 使用互斥锁(分布式锁)

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 总结通过这篇文章,我们系统地探讨了工作中最常用的六种缓存技术。

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

本地缓存:适合进程内、变化不频繁的只读数据Redis:功能丰富的分布式缓存,适合大多数共享缓存场景Memcached:简单高效的分布式缓存,适合纯Key-Value场景CDN缓存:加速静态资源,提升全球访问速度浏览器缓存:最前端的优化,减少不必要的网络请求数据库缓存:最后一道防线,优化数据库访问性能缓存使用的核心原则可以总结为以下几点:

分级缓存:合理利用多级缓存架构合适粒度:根据业务特点选择缓存粒度及时更新:设计合理的缓存更新策略监控告警:建立完善的缓存监控体系有些小伙伴在工作中使用缓存时,容易陷入两个极端:要么过度设计,所有数据都加缓存;要么忽视缓存,让数据库承受所有压力。

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

记住,缓存不是银弹,而是工具箱中的一件利器。

代码语言:javascript复制系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

Copyright © 2088 竞技新视野 - 电竞赛事活动专题站 All Rights Reserved.
友情链接