缓存系统设计与预热策略
缓存系统设计要素
缓存是提升系统性能的关键技术,但设计一个健壮的缓存系统需要考虑多个维度。根据部署位置的不同,缓存分为本地缓存和分布式缓存两大类,它们面临的挑战和解决方案各有不同。
graph TD
A[缓存系统设计要素] --> B[本地缓存]
A --> C[分布式缓存]
B --> B1[数据结构]
B --> B2[线程安全]
B --> B3[容量限制]
B --> B4[淘汰策略]
B --> B5[过期机制]
C --> C1[数据结构]
C --> C2[线程安全]
C --> C3[淘汰策略]
C --> C4[持久化机制]
C --> C5[集群模式]
C --> C6[过期策略]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#27AE60,color:#fff,rx:10,ry:10
style C fill:#E67E22,color:#fff,rx:10,ry:10本地缓存核心设计
数据结构选择
缓存的本质是空间换时间,通过在内存中存储热点数据来避免昂贵的IO操作。为实现O(1)的存取效率,通常采用Key-Value结构:
- HashMap系列:基于哈希表实现,适合大多数场景
- ConcurrentHashMap:线程安全的哈希表,适合多线程环境
- 专用缓存库:如Caffeine、Guava Cache,提供更丰富的功能
线程安全保障
本地缓存通常作为全局共享资源,必须考虑并发访问安全:
// 不推荐:使用普通HashMap
private Map<String, Object> cache = new HashMap<>(); // 线程不安全
// 推荐方式一:使用ConcurrentHashMap
private Map<String, Object> cache = new ConcurrentHashMap<>();
// 推荐方式二:使用Caffeine
private Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();优秀的缓存框架通常采用以下技术保证线程安全:
- CAS操作:无锁更新计数器、统计数据等
- 分段锁:将数据分成多个段,降低锁粒度
- 读写分离:允许并发读,串行写
容量控制机制
本地缓存占用JVM堆内存,必须设置上限防止OOM:
// 方式一:限制条目数量
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10000) // 最多存储10000个条目
.build();
// 方式二:基于权重限制
Cache<String, byte[]> cache = Caffeine.newBuilder()
.maximumWeight(100 * 1024 * 1024) // 总权重不超过100MB
.weigher((key, value) -> value.length) // 以字节数组长度为权重
.build();淘汰策略
当缓存达到容量上限时,需要淘汰部分数据。常见淘汰策略:
graph LR
A[淘汰策略] --> B[FIFO<br/>先进先出]
A --> C[LRU<br/>最近最少使用]
A --> D[LFU<br/>最不经常使用]
A --> E[SOFT<br/>软引用]
A --> F[WEAK<br/>弱引用]
B --> B1[按入队顺序淘汰<br/>简单但命中率低]
C --> C1[淘汰最久未访问的<br/>适合热点明显场景]
D --> D1[淘汰访问次数最少的<br/>适合访问频率差异大]
E --> E1[内存不足时GC回收<br/>适合大对象缓存]
F --> F1[GC时必定回收<br/>适合临时缓存]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style C fill:#27AE60,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10Caffeine采用Window TinyLFU算法,结合LRU和LFU的优点:
- 新数据进入一个小型的Window Cache(基于LRU)
- 从Window淘汰的数据与Main Cache的候选数据比较访问频率
- 频率更高的数据进入Main Cache
- 通过Count-Min Sketch高效统计访问频率
过期时间设计
过期时间是确保数据新鲜度的最后保障:
Cache<String, String> cache = Caffeine.newBuilder()
// 写入后10分钟过期
.expireAfterWrite(Duration.ofMinutes(10))
// 或:最后访问后5分钟过期
.expireAfterAccess(Duration.ofMinutes(5))
// 或:自定义过期策略
.expireAfter(new Expiry<String, String>() {
@Override
public long expireAfterCreate(String key, String value, long currentTime) {
// 根据key动态设置过期时间
return key.startsWith("hot:")
? TimeUnit.HOURS.toNanos(1)
: TimeUnit.MINUTES.toNanos(10);
}
// ...其他方法
})
.build();分布式缓存扩展设计
分布式缓存(如Redis)在本地缓存设计要素基础上,还需要考虑数据持久化和集群管理。
持久化机制
Redis提供两种持久化方式:
RDB快照:定时生成数据快照,fork子进程执行,恢复速度快,但可能丢失最后一次快照后的数据。
graph TD
A[RDB快照] --> B[定时生成数据快照]
B --> C[fork子进程执行]
C --> D[恢复速度快]
D --> E[可能丢失部分数据]
style A fill:#E67E22,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10
style E fill:#E74C3C,color:#fff,rx:10,ry:10AOF日志:记录每个写命令,追加写入日志文件,数据安全性高,但文件较大、恢复较慢。
graph TD
A[AOF日志] --> B[记录每个写命令]
B --> C[追加写入日志文件]
C --> D[数据安全性高]
D --> E[文件较大,恢复较慢]
style A fill:#27AE60,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10
style E fill:#E74C3C,color:#fff,rx:10,ry:10混合持久化:结合RDB快照与AOF增量日志,兼顾恢复速度和数据安全。
graph TD
A[混合持久化] --> B[RDB快照 + AOF增量]
B --> C[兼顾恢复速度和数据安全]
style A fill:#9B59B6,color:#fff,rx:10,ry:10
style C fill:#27AE60,color:#fff,rx:10,ry:10集群模式
Redis集群解决单节点容量和性能瓶颈:
- 主从复制:数据备份,读写分离
- 哨兵模式:自动故障转移
- Cluster模式:数据分片,水平扩展
缓存预热策略
缓存预热是在系统启动或业务高峰到来之前,主动将热点数据加载到缓存中,避免缓存冷启动导致的大量请求穿透到数据库。
预热时机选择
应用启动时预热:利用Spring生命周期事件,适合本地缓存,数据量不宜过大。
graph TD
A[应用启动时] --> B[Spring生命周期事件]
B --> C[适合本地缓存]
C --> D[数据量不宜过大]
style A fill:#27AE60,color:#fff,rx:10,ry:10
style B fill:#4A90E2,color:#fff,rx:10,ry:10定时任务预热:定时刷新热点数据,保持数据新鲜度,适合周期性变化的数据。
graph TD
A[定时任务] --> B[定时刷新热点数据]
B --> C[保持数据新鲜度]
C --> D[适合周期性变化的数据]
style A fill:#E67E22,color:#fff,rx:10,ry:10
style B fill:#4A90E2,color:#fff,rx:10,ry:10按需加载:首次访问时加载,实现简单,但首次访问会较慢。
graph TD
A[按需加载] --> B[首次访问时加载]
B --> C[实现简单]
C --> D[首次访问会较慢]
style A fill:#9B59B6,color:#fff,rx:10,ry:10
style C fill:#27AE60,color:#fff,rx:10,ry:10
style D fill:#E74C3C,color:#fff,rx:10,ry:10缓存加载器:框架自动触发,访问时自动加载,支持异步刷新。
graph TD
A[缓存加载器] --> B[框架自动触发]
B --> C[访问时自动加载]
C --> D[支持异步刷新]
style A fill:#E74C3C,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10启动时预热
基于Spring生命周期在应用启动完成后自动加载缓存:
方式一:ApplicationReadyEvent
@Component
public class CacheWarmer {
@Autowired
private ProductService productService;
@Autowired
private Cache<String, Product> productCache;
@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
log.info("开始缓存预热...");
// 加载热门商品到缓存
List<Product> hotProducts = productService.getHotProducts(1000);
for (Product product : hotProducts) {
productCache.put(product.getId(), product);
}
log.info("缓存预热完成,共加载{}个商品", hotProducts.size());
}
}方式二:CommandLineRunner
@Component
public class CacheWarmRunner implements CommandLineRunner {
@Autowired
private ConfigService configService;
@Override
public void run(String... args) {
// 加载系统配置到本地缓存
List<SystemConfig> configs = configService.loadAllConfigs();
configs.forEach(config ->
LocalCache.put(config.getKey(), config.getValue())
);
}
}方式三:InitializingBean
@Service
public class CategoryCacheService implements InitializingBean {
@Autowired
private CategoryMapper categoryMapper;
private Map<String, Category> categoryCache = new ConcurrentHashMap<>();
@Override
public void afterPropertiesSet() {
// Bean初始化完成后加载分类数据
List<Category> categories = categoryMapper.selectAll();
categories.forEach(cat -> categoryCache.put(cat.getId(), cat));
}
public Category getCategory(String id) {
return categoryCache.get(id);
}
}定时任务预热
对于需要周期性更新的缓存数据,使用定时任务刷新:
@Component
public class ScheduledCacheRefresher {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RankingService rankingService;
/**
* 每天凌晨2点刷新排行榜缓存
*/
@Scheduled(cron = "0 0 2 * * ?")
public void refreshRankingCache() {
log.info("开始刷新排行榜缓存");
// 计算各类排行榜
List<RankItem> salesRank = rankingService.calculateSalesRanking();
List<RankItem> viewRank = rankingService.calculateViewRanking();
// 更新到Redis
redisTemplate.opsForValue().set("rank:sales",
JSON.toJSONString(salesRank),
Duration.ofDays(1));
redisTemplate.opsForValue().set("rank:view",
JSON.toJSONString(viewRank),
Duration.ofDays(1));
log.info("排行榜缓存刷新完成");
}
/**
* 每小时刷新热点商品缓存
*/
@Scheduled(fixedRate = 3600000)
public void refreshHotProductCache() {
// 获取最近一小时的热门商品
List<String> hotProductIds = statisticsService.getHotProductIds(100);
// 批量加载商品详情并预热
List<Product> products = productService.batchGetProducts(hotProductIds);
products.forEach(product ->
redisTemplate.opsForValue().set(
"product:" + product.getId(),
JSON.toJSONString(product),
Duration.ofHours(2)
)
);
}
}按需加载(Lazy Loading)
首次访问时触发缓存加载,是最常见的缓存策略:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private StringRedisTemplate redisTemplate;
public User getUser(String userId) {
String cacheKey = "user:" + userId;
// 先查缓存
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 缓存未命中,查询数据库
User user = userMapper.selectById(userId);
if (user != null) {
// 写入缓存,设置过期时间
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(user),
Duration.ofMinutes(30)
);
}
return user;
}
}缓存加载器模式
Caffeine等现代缓存框架支持缓存加载器,自动处理缓存未命中的情况:
@Service
public class ConfigCacheService {
@Autowired
private ConfigMapper configMapper;
private final LoadingCache<String, String> configCache;
public ConfigCacheService() {
this.configCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(30))
// 配置自动刷新
.refreshAfterWrite(Duration.ofMinutes(10))
// 设置加载器
.build(this::loadConfigFromDB);
}
/**
* 缓存未命中时自动调用此方法加载
*/
private String loadConfigFromDB(String configKey) {
log.info("从数据库加载配置: {}", configKey);
SystemConfig config = configMapper.selectByKey(configKey);
return config != null ? config.getValue() : null;
}
public String getConfig(String configKey) {
// get方法会自动触发加载器
return configCache.get(configKey);
}
}加载器模式的优势:
- 代码更简洁,无需手动处理缓存穿透
- 支持异步刷新,不阻塞读请求
- 自动处理并发加载,避免缓存击穿
多级缓存预热
对于关键业务,需要同时预热本地缓存和分布式缓存:
graph TD
A[预热流程] --> B[加载数据源]
B --> C[写入Redis集群]
C --> D[同步到本地缓存]
E[请求流程] --> F{本地缓存}
F -->|命中| G[返回结果]
F -->|未命中| H{Redis缓存}
H -->|命中| I[回写本地缓存]
I --> G
H -->|未命中| J[查询数据库]
J --> K[写入Redis]
K --> I
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style C fill:#E67E22,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10
style F fill:#27AE60,color:#fff,rx:10,ry:10
style H fill:#E67E22,color:#fff,rx:10,ry:10@Component
public class MultiLevelCacheWarmer {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductMapper productMapper;
// 本地缓存
private final Cache<String, Product> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
@EventListener(ApplicationReadyEvent.class)
public void warmUp() {
log.info("开始多级缓存预热");
// 1. 获取热点商品ID列表
List<String> hotIds = getHotProductIds();
// 2. 批量加载商品数据
List<Product> products = productMapper.batchSelect(hotIds);
// 3. 写入Redis
Map<String, String> redisData = new HashMap<>();
for (Product product : products) {
String key = "product:" + product.getId();
redisData.put(key, JSON.toJSONString(product));
// 4. 同时写入本地缓存
localCache.put(product.getId(), product);
}
redisTemplate.opsForValue().multiSet(redisData);
// 5. 设置过期时间
hotIds.forEach(id ->
redisTemplate.expire("product:" + id, Duration.ofHours(2))
);
log.info("多级缓存预热完成,共预热{}个商品", products.size());
}
/**
* 查询商品(优先本地缓存)
*/
public Product getProduct(String productId) {
// 一级:本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// 二级:Redis缓存
String cached = redisTemplate.opsForValue().get("product:" + productId);
if (cached != null) {
product = JSON.parseObject(cached, Product.class);
// 回填本地缓存
localCache.put(productId, product);
return product;
}
// 三级:数据库
product = productMapper.selectById(productId);
if (product != null) {
// 写入两级缓存
String json = JSON.toJSONString(product);
redisTemplate.opsForValue().set("product:" + productId, json, Duration.ofHours(2));
localCache.put(productId, product);
}
return product;
}
}预热注意事项
避免启动阻塞
预热数据量大时可能导致应用启动缓慢,可采用异步预热:
@EventListener(ApplicationReadyEvent.class)
public void warmUpAsync() {
// 异步执行预热,不阻塞应用启动
CompletableFuture.runAsync(() -> {
try {
doWarmUp();
} catch (Exception e) {
log.error("缓存预热失败", e);
}
});
}预热进度监控
对于大规模预热,需要监控进度和耗时:
public void warmUpWithProgress() {
List<String> allIds = getAllProductIds();
int total = allIds.size();
AtomicInteger progress = new AtomicInteger(0);
// 分批预热
Lists.partition(allIds, 100).forEach(batch -> {
warmUpBatch(batch);
int current = progress.addAndGet(batch.size());
log.info("预热进度: {}/{} ({}%)", current, total, current * 100 / total);
});
}预热失败处理
预热失败不应影响应用正常启动和服务:
@EventListener(ApplicationReadyEvent.class)
public void safeWarmUp() {
try {
warmUpCache();
} catch (Exception e) {
// 记录错误但不抛出,允许应用继续运行
log.error("缓存预热失败,将依赖按需加载", e);
// 可选:发送告警通知
alertService.sendAlert("缓存预热失败", e.getMessage());
}
}更新: 2025-12-04 17:42:35
原文: https://www.yuque.com/u22210564/zoxfmt/doc-30-04