缓存构建策略
1. 四种缓存构建模式总览
Section titled “1. 四种缓存构建模式总览”| 模式 | 读写方向 | 一致性 | 适用场景 |
|---|---|---|---|
| Cache Aside(旁路缓存) | 应用自己管理缓存 | 最终一致 | 通用场景,最常用 |
| Read/Write Through(直写) | 缓存层代理 DB 读写 | 较强一致 | 写多读多,缓存中间件支持 |
| Write Behind(异步回写) | 写缓存,异步刷 DB | 弱一致 | 高写吞吐,允许短暂不一致 |
| Refresh Ahead(预热/提前刷新) | 后台定时重建缓存 | 强一致(不过期) | 热点数据,低延迟要求 |
缓存构建全景:
Cache Aside ──▶ 应用 → 缓存 → DB(miss 时)Read Through ──▶ 应用 → 缓存(缓存自己去读 DB)Write Through──▶ 应用 → 缓存 → DB(同步写)Write Behind ──▶ 应用 → 缓存 → 异步队列 → DBRefresh Ahead──▶ 定时任务提前刷新即将过期的热点 key2. Cache Aside(旁路缓存)
Section titled “2. Cache Aside(旁路缓存)”2.1. 原理
Section titled “2.1. 原理”最常见的缓存策略,应用层自己负责缓存的读、写、失效逻辑。
读流程:
请求 ──▶ 查缓存 │ ├── 命中 ──▶ 返回缓存数据 └── 未命中 ──▶ 查 DB ──▶ 写入缓存 ──▶ 返回写流程(删除缓存,而非更新):
写请求 ──▶ 更新 DB ──▶ 删除缓存(而非写缓存)2.2. 代码实现
Section titled “2.2. 代码实现”@Service@RequiredArgsConstructorpublic class ProductService {
private final StringRedisTemplate redisTemplate; private final ProductMapper productMapper; private final ObjectMapper objectMapper;
private static final long CACHE_TTL = 30L; private static final String KEY_PREFIX = "product:";
// 读:先查缓存,miss 时查 DB 并回填 public Product getById(Long id) throws JsonProcessingException { String key = KEY_PREFIX + id; String json = redisTemplate.opsForValue().get(key); if (json != null) { return objectMapper.readValue(json, Product.class); } Product product = productMapper.selectById(id); if (product != null) { redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(product), CACHE_TTL, TimeUnit.MINUTES); } return product; }
// 写:先更新 DB,再删除缓存 @Transactional public void update(Product product) { productMapper.updateById(product); redisTemplate.delete(KEY_PREFIX + product.getId()); // 删缓存 }}2.3. 先删缓存还是先更新 DB?
Section titled “2.3. 先删缓存还是先更新 DB?”方案 A:先删缓存,再更新 DB(❌ 存在脏读窗口) 线程A:删缓存 ──────────────────── 更新DB 线程B: 查缓存(miss) ──▶ 查DB(旧值) ──▶ 写入缓存(旧值) ← 脏数据!
方案 B:先更新 DB,再删缓存(✅ 推荐) 线程A:更新DB ──▶ 删缓存 线程B:查缓存(命中旧值) ── 正常,下次请求会加载新值 (极短不一致窗口,且下次请求即可修复)3. Read / Write Through(直写穿透)
Section titled “3. Read / Write Through(直写穿透)”3.1. 原理
Section titled “3.1. 原理”应用只和缓存层交互,由缓存层自动代理对 DB 的读写,对应用透明。
Read Through:应用 ──▶ 缓存层 │ ├── 命中 ──▶ 返回 └── miss ──▶ 缓存自己查 DB ──▶ 回填缓存 ──▶ 返回
Write Through:应用 ──▶ 缓存层 ──▶ 同步写 DB(缓存和 DB 同步更新)3.2. Spring Cache 模拟实现
Section titled “3.2. Spring Cache 模拟实现”Spring 的 @Cacheable / @CachePut / @CacheEvict 是 Read/Write Through 的常见近似实现:
@Service@CacheConfig(cacheNames = "products")public class ProductService {
// Read Through:miss 时自动查 DB 并缓存 @Cacheable(key = "#id") public Product getById(Long id) { return productMapper.selectById(id); }
// Write Through:更新 DB 并同步更新缓存 @CachePut(key = "#product.id") public Product update(Product product) { productMapper.updateById(product); return product; }
// 失效缓存 @CacheEvict(key = "#id") public void delete(Long id) { productMapper.deleteById(id); }}3.3. 配置 Redis CacheManager
Section titled “3.3. 配置 Redis CacheManager”@Configuration@EnableCachingpublic class CacheConfig {
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) // 默认 TTL 30 分钟 .serializeKeysWith(RedisSerializationContext .SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext .SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); }}4. Write Behind(异步回写)
Section titled “4. Write Behind(异步回写)”4.1. 原理
Section titled “4.1. 原理”写操作只更新缓存,由后台异步任务批量将缓存数据刷回 DB。牺牲一致性换取极高的写吞吐量。
写请求 ──▶ 更新缓存 ──▶ 立即返回(不等 DB) │ 后台线程(定时 / 批量) │ ▼ 批量写入 DB4.2. 适用场景与风险
Section titled “4.2. 适用场景与风险”| 适用场景 | 风险 |
|---|---|
| 计数器(点赞数、浏览量) | 缓存宕机,未刷 DB 的数据丢失 |
| 高频写低频读的统计数据 | 数据库和缓存存在时间窗口不一致 |
| 允许最终一致的业务 | 实现复杂,需自行处理失败重试 |
4.3. 简单实现(Redis + 定时任务)
Section titled “4.3. 简单实现(Redis + 定时任务)”@Service@RequiredArgsConstructorpublic class ViewCountService {
private final StringRedisTemplate redisTemplate; private final ArticleMapper articleMapper;
private static final String KEY = "view:count:";
// 写:只写 Redis,不写 DB public void increment(Long articleId) { redisTemplate.opsForValue().increment(KEY + articleId); }
// 读:从 Redis 读取 public long getCount(Long articleId) { String val = redisTemplate.opsForValue().get(KEY + articleId); return val == null ? 0L : Long.parseLong(val); }
// 后台定时刷回 DB(每 5 分钟) @Scheduled(fixedDelay = 5 * 60 * 1000) public void flushToDB() { Set<String> keys = redisTemplate.keys(KEY + "*"); if (keys == null) return; for (String key : keys) { Long articleId = Long.parseLong(key.replace(KEY, "")); String val = redisTemplate.opsForValue().getAndDelete(key); if (val != null) { articleMapper.incrementViewCount(articleId, Long.parseLong(val)); } } }}5. Refresh Ahead(预热与提前刷新)
Section titled “5. Refresh Ahead(预热与提前刷新)”5.1. 原理
Section titled “5.1. 原理”对热点数据提前刷新缓存,使其永不过期(或在过期前就被重建),避免缓存击穿和冷启动问题。
定时任务 / 消息触发 │ ▼主动查询 DB ──▶ 写入缓存(重置 TTL 或使用逻辑过期) │ ▼请求到来时缓存始终命中(无 miss)5.2. 启动预热
Section titled “5.2. 启动预热”@Component@RequiredArgsConstructor@Slf4jpublic class CacheWarmup implements ApplicationRunner {
private final StringRedisTemplate redisTemplate; private final ProductMapper productMapper; private final ObjectMapper objectMapper;
@Override public void run(ApplicationArguments args) throws Exception { // 应用启动时预热 Top 1000 热点商品 List<Product> hotProducts = productMapper.selectTop1000(); for (Product p : hotProducts) { redisTemplate.opsForValue().set( "product:" + p.getId(), objectMapper.writeValueAsString(p), 24, TimeUnit.HOURS ); } log.info("缓存预热完成,共加载 {} 条", hotProducts.size()); }}5.3. 定时刷新(结合逻辑过期)
Section titled “5.3. 定时刷新(结合逻辑过期)”与「[[1. 缓存三大问题]]」中的逻辑过期方案配合,实现热点数据永不过期:
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨 3 点全量刷新public void refreshHotProductCache() throws JsonProcessingException { List<Product> hotList = productMapper.selectTop1000(); for (Product p : hotList) { // 逻辑过期时间设为 25 小时,比刷新周期(24h)长,确保不存在过期空窗 CacheWrapper<Product> wrapper = new CacheWrapper<>( p, LocalDateTime.now().plusHours(25) ); redisTemplate.opsForValue().set( "product:" + p.getId(), objectMapper.writeValueAsString(wrapper) // 不设置 TTL,Redis 层永不过期,靠逻辑过期时间判断是否需要刷新 ); }}6. 四种模式对比与选型
Section titled “6. 四种模式对比与选型”| 对比项 | Cache Aside | Read/Write Through | Write Behind | Refresh Ahead |
|---|---|---|---|---|
| 实现复杂度 | 低 | 中 | 高 | 中 |
| 读性能 | 高(命中时) | 高 | 高 | 极高(无 miss) |
| 写性能 | 中(同步删缓存) | 中(同步写 DB) | 极高(异步) | 高 |
| 数据一致性 | 最终一致 | 较强一致 | 弱一致 | 强一致(热点) |
| 数据丢失风险 | 无 | 无 | 有(Redis 宕机) | 无 |
| 推荐场景 | 通用业务 | 读写均衡业务 | 高频写统计数据 | 热点数据低延迟 |
选型决策树:
有专门的缓存中间件支持?├── 是 ──▶ Read/Write Through└── 否 │ 写操作频率极高且允许短暂数据丢失? ├── 是 ──▶ Write Behind └── 否 │ 是热点数据且不允许 miss? ├── 是 ──▶ Refresh Ahead(预热 + 定时刷新) └── 否 ──▶ Cache Aside(默认选择)7. 生产环境推荐组合
Section titled “7. 生产环境推荐组合”通用业务(电商、CMS):Cache Aside(主体) + 布隆过滤器(防穿透,见「[[2. 布隆过滤器]]」) + 互斥锁 / 逻辑过期(防击穿,见「[[1. 缓存三大问题]]」) + 随机 TTL(防雪崩)
高频写统计(计数、排行榜):Write Behind(Redis 计数)+ 定时刷 DB
热点数据(首页、Banner):Refresh Ahead(预热)+ 逻辑过期(不设 TTL)