跳转到内容

缓存构建策略

模式读写方向一致性适用场景
Cache Aside(旁路缓存)应用自己管理缓存最终一致通用场景,最常用
Read/Write Through(直写)缓存层代理 DB 读写较强一致写多读多,缓存中间件支持
Write Behind(异步回写)写缓存,异步刷 DB弱一致高写吞吐,允许短暂不一致
Refresh Ahead(预热/提前刷新)后台定时重建缓存强一致(不过期)热点数据,低延迟要求
缓存构建全景:
Cache Aside ──▶ 应用 → 缓存 → DB(miss 时)
Read Through ──▶ 应用 → 缓存(缓存自己去读 DB)
Write Through──▶ 应用 → 缓存 → DB(同步写)
Write Behind ──▶ 应用 → 缓存 → 异步队列 → DB
Refresh Ahead──▶ 定时任务提前刷新即将过期的热点 key

最常见的缓存策略,应用层自己负责缓存的读、写、失效逻辑。

读流程:

请求 ──▶ 查缓存
│
├── 命中 ──▶ 返回缓存数据
└── 未命中 ──▶ 查 DB ──▶ 写入缓存 ──▶ 返回

写流程(删除缓存,而非更新):

写请求 ──▶ 更新 DB ──▶ 删除缓存(而非写缓存)
@Service
@RequiredArgsConstructor
public 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()); // 删缓存
}
}
方案 A:先删缓存,再更新 DB(❌ 存在脏读窗口)
线程A:删缓存 ──────────────────── 更新DB
线程B: 查缓存(miss) ──▶ 查DB(旧值) ──▶ 写入缓存(旧值) ← 脏数据!
方案 B:先更新 DB,再删缓存(✅ 推荐)
线程A:更新DB ──▶ 删缓存
线程B:查缓存(命中旧值) ── 正常,下次请求会加载新值
(极短不一致窗口,且下次请求即可修复)

应用只和缓存层交互,由缓存层自动代理对 DB 的读写,对应用透明。

Read Through:
应用 ──▶ 缓存层
│
├── 命中 ──▶ 返回
└── miss ──▶ 缓存自己查 DB ──▶ 回填缓存 ──▶ 返回
Write Through:
应用 ──▶ 缓存层 ──▶ 同步写 DB(缓存和 DB 同步更新)

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);
}
}
@Configuration
@EnableCaching
public 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();
}
}

写操作只更新缓存,由后台异步任务批量将缓存数据刷回 DB。牺牲一致性换取极高的写吞吐量。

写请求 ──▶ 更新缓存 ──▶ 立即返回(不等 DB)
│
后台线程(定时 / 批量)
│
▼
批量写入 DB
适用场景风险
计数器(点赞数、浏览量)缓存宕机,未刷 DB 的数据丢失
高频写低频读的统计数据数据库和缓存存在时间窗口不一致
允许最终一致的业务实现复杂,需自行处理失败重试

4.3. 简单实现(Redis + 定时任务)

Section titled “4.3. 简单实现(Redis + 定时任务)”
@Service
@RequiredArgsConstructor
public 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(预热与提前刷新)”

对热点数据提前刷新缓存,使其永不过期(或在过期前就被重建),避免缓存击穿和冷启动问题。

定时任务 / 消息触发
│
▼
主动查询 DB ──▶ 写入缓存(重置 TTL 或使用逻辑过期)
│
▼
请求到来时缓存始终命中(无 miss)
@Component
@RequiredArgsConstructor
@Slf4j
public 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());
}
}

与「[[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 层永不过期,靠逻辑过期时间判断是否需要刷新
);
}
}
对比项Cache AsideRead/Write ThroughWrite BehindRefresh Ahead
实现复杂度低中高中
读性能高(命中时)高高极高(无 miss)
写性能中(同步删缓存)中(同步写 DB)极高(异步)高
数据一致性最终一致较强一致弱一致强一致(热点)
数据丢失风险无无有(Redis 宕机)无
推荐场景通用业务读写均衡业务高频写统计数据热点数据低延迟
选型决策树:
有专门的缓存中间件支持?
├── 是 ──▶ Read/Write Through
└── 否
│
写操作频率极高且允许短暂数据丢失?
├── 是 ──▶ Write Behind
└── 否
│
是热点数据且不允许 miss?
├── 是 ──▶ Refresh Ahead(预热 + 定时刷新)
└── 否 ──▶ Cache Aside(默认选择)
通用业务(电商、CMS):
Cache Aside(主体)
+ 布隆过滤器(防穿透,见「[[2. 布隆过滤器]]」)
+ 互斥锁 / 逻辑过期(防击穿,见「[[1. 缓存三大问题]]」)
+ 随机 TTL(防雪崩)
高频写统计(计数、排行榜):
Write Behind(Redis 计数)+ 定时刷 DB
热点数据(首页、Banner):
Refresh Ahead(预热)+ 逻辑过期(不设 TTL)