标签:Cache, 缓存策略, Redis, Java, Jedis, Cache-Aside, Write-Through, Write-Back, 系统设计, 架构设计, 性能优化, 数据一致性
本文给出可直接复制运行的 Java 示例代码,涵盖常用的缓存读写策略:Cache-Aside(旁路缓存)、Write-Through(写穿)、Write-Around(写绕过)、Write-Back(写回/延迟写),并补充双删(Double Delete)、提前刷新(Refresh Ahead)与TTL示例。示例使用 Jedis 作为 Redis 客户端,数据库以伪 UserRepository
(可替换为实际 JDBC / JPA 实现)表示。代码注释尽量详细,便于工程落地。
注意:以下示例都是较为精简的工程级样板,生产环境需要考虑异常重试、监控、幂等、序列化策略、连接池、断路器与分布式锁等。
依赖(Maven)
<dependencies>
<!-- Jedis Redis 客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.3</version>
</dependency>
<!-- 可替换为你实际使用的 JDBC 或 JPA 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.27</version>
</dependency>
<!-- JSON 序列化(示例使用 Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
公共类与工具(序列化、Redis client、Repository 接口)
// User.java - 简单实体
public class User {
private Long id;
private String name;
private int age;
// getters/setters, constructors, toString...
}
// RedisClientFactory.java - Jedis 单例工厂(示例)
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisClientFactory {
private static final JedisPool pool;
static {
JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxTotal(50);
pool = new JedisPool(cfg, "127.0.0.1", 6379);
}
public static JedisPool getPool() { return pool; }
}
// JsonUtil.java - Jackson 简单封装
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonUtil {
private static final ObjectMapper M = new ObjectMapper();
public static String toJson(Object o){ try{ return M.writeValueAsString(o);}catch(Exception e){throw new RuntimeException(e);} }
public static <T> T fromJson(String s, Class<T> cls){ try{ return M.readValue(s, cls);}catch(Exception e){throw new RuntimeException(e);} }
}
// UserRepository.java - 伪数据库操作接口(替换为真实实现)
public interface UserRepository {
User findById(Long id);
void save(User user);
void update(User user);
void delete(Long id);
}
1) Cache-Aside(旁路缓存) — 最常见的读策略(读写分离由应用控制)
读取时:先读 Redis,未命中则回库并回填缓存。写入时更新 DB 并主动删除(或更新)缓存(双删/更新策略见后)。
import redis.clients.jedis.Jedis;
public class UserCacheAsideService {
private final UserRepository repo;
private final JedisPool pool;
private final long ttlSeconds = 60 * 5; // 5分钟
public UserCacheAsideService(UserRepository repo) {
this.repo = repo;
this.pool = RedisClientFactory.getPool();
}
private String key(Long id){ return "user:" + id; }
// 读:Cache-Aside
public User getUser(Long id){
try (Jedis jedis = pool.getResource()){
String json = jedis.get(key(id));
if (json != null){
return JsonUtil.fromJson(json, User.class);
}
}
// 缓存未命中 -> 从 DB 加载并回填
User user = repo.findById(id);
if (user != null){
try (Jedis jedis = pool.getResource()){
jedis.setex(key(id), (int)ttlSeconds, JsonUtil.toJson(user));
}
}
return user;
}
// 写:先更新 DB,再删除缓存(简单双删)
public void updateUser(User user){
repo.update(user);
try (Jedis jedis = pool.getResource()){
jedis.del(key(user.getId())); // 删除缓存,下一次读会回填
}
}
}
要点:实现简单,适合读多写少场景。更新时用“先 DB 后删缓存”或“双删”来减少并发脏读。
2) Write-Through(写穿) — 每次写同时写缓存与数据库(同步)
实现方式通常把写操作封装在缓存代理层:写缓存时同步写 DB(或由缓存代理写 DB)。
public class UserWriteThroughService {
private final UserRepository repo;
private final JedisPool pool;
private final long ttlSeconds = 60 * 10;
public UserWriteThroughService(UserRepository repo){
this.repo = repo;
this.pool = RedisClientFactory.getPool();
}
// 写穿:应用调用此方法,方法内部先写缓存再写DB(同步)
public void saveOrUpdate(User user){
String key = "user:" + user.getId();
try (Jedis jedis = pool.getResource()){
jedis.setex(key, (int)ttlSeconds, JsonUtil.toJson(user));
// 接下来同步写DB —— 保证一致性
repo.save(user); // 或 update,根据ID
}
}
// 读直接从缓存(或按需求回源)
public User getUser(Long id){
try (Jedis jedis = pool.getResource()){
String json = jedis.get("user:" + id);
if (json != null) return JsonUtil.fromJson(json, User.class);
}
// 极端情况下可回源
return repo.findById(id);
}
}
要点:强一致性,读到的缓存数据与 DB 保持同步。缺点:写延迟较高;写放大。
3) Write-Around(写绕过) — 写直接到 DB(不更新缓存)
写时绕过缓存,避免缓存被冷写入;读时仍用 Cache-Aside 回填。
public class UserWriteAroundService {
private final UserRepository repo;
private final JedisPool pool;
private final long ttlSeconds = 60 * 5;
public UserWriteAroundService(UserRepository repo){
this.repo = repo;
this.pool = RedisClientFactory.getPool();
}
// 写直接到 DB,不更新缓存
public void saveOrUpdate(User user){
repo.save(user);
// 不主动更新 / 删除缓存(或可选择删除以防读到旧值)
// jedis.del(key) 可选
}
// 读仍然走 Cache-Aside(见上)
public User getUser(Long id){
try (Jedis jedis = pool.getResource()){
String json = jedis.get("user:" + id);
if (json != null) return JsonUtil.fromJson(json, User.class);
}
User user = repo.findById(id);
if (user != null){
try (Jedis jedis = pool.getResource()){
jedis.setex("user:" + id, (int)ttlSeconds, JsonUtil.toJson(user));
}
}
return user;
}
}
要点:写性能高、避免缓存污染;但首次读会比较慢,可能读到旧数据(如果写后没有删缓存)。
4) Write-Back(写回 / 延迟写) — 高吞吐写场景(复杂)
写入先写缓存并标记脏(dirty),后台异步批量落库(write-behind)。风险:缓存崩溃可能丢数据。下面示例使用内存队列 + 后台线程批量 flush。
import java.util.concurrent.*;
public class UserWriteBackService {
private final UserRepository repo;
private final JedisPool pool;
private final BlockingQueue<User> writeQueue = new LinkedBlockingQueue<>();
private final ScheduledExecutorService flusher = Executors.newSingleThreadScheduledExecutor();
private final long ttlSeconds = 60 * 5;
public UserWriteBackService(UserRepository repo){
this.repo = repo;
this.pool = RedisClientFactory.getPool();
// 每秒批量写回 DB(示例)
flusher.scheduleAtFixedRate(this::flushToDb, 1, 1, TimeUnit.SECONDS);
}
// 写:写入缓存并入队(标记脏)
public void saveOrUpdate(User user){
try (Jedis jedis = pool.getResource()){
jedis.setex("user:" + user.getId(), (int)ttlSeconds, JsonUtil.toJson(user));
// 标记:可以用一个集合保存脏 key(示例用队列)
writeQueue.offer(user);
}
}
// 后台批量提交到 DB
private void flushToDb(){
List<User> batch = new ArrayList<>();
writeQueue.drainTo(batch, 100); // 每次最多100条
if (batch.isEmpty()) return;
for (User u : batch){
try {
repo.update(u); // 真实场景考虑事务与幂等
} catch (Exception e){
// 出错需重试/日志/告警 —— 简化处理:重新放回队列
writeQueue.offer(u);
}
}
}
// 关闭时记得shutdown并做一次 flush
public void shutdown(){
flusher.shutdown();
flushToDb();
}
}
要点与保障措施:
- 必须保证写队列持久化(内存队列容易丢数据)。生产一般用 Kafka/Redis Stream/RDB 队列做持久化缓冲。
- 异常时要有重试、幂等写、持久化队列与监控告警。
- 对强一致性场景不适用。
5) 双删策略(Double Delete) — 缓解并发写读顺序问题
场景:A 更新 DB 后,B 读老数据再写缓存导致脏数据。双删流程常为:先删缓存 → 更新 DB → 等短延迟 → 再删缓存一次。
public class DoubleDeleteExample {
private final JedisPool pool;
private final UserRepository repo;
public DoubleDeleteExample(UserRepository repo) {
this.repo = repo;
this.pool = RedisClientFactory.getPool();
}
public void updateUser(User user){
String key = "user:" + user.getId();
try (Jedis jedis = pool.getResource()){
jedis.del(key); // 第一次删
}
repo.update(user); // 更新 DB
// 等个短暂延迟后再删一次(避免并发回写)
ScheduledExecutorService s = Executors.newSingleThreadScheduledExecutor();
s.schedule(() -> {
try (Jedis jedis = pool.getResource()){
jedis.del(key); // 第二次删
} finally {
s.shutdown();
}
}, 200, TimeUnit.MILLISECONDS);
}
}
要点:双删不能完全消除竞态,仅降低概率。结合分布式锁或消息驱动刷新更可靠。
6) Refresh Ahead(提前刷新)— 为热点提前续期,避免缓存击穿
思路:后台定时任务在 TTL 快到时异步回源并更新缓存,使请求不会触发回源。
public class RefreshAheadService {
private final JedisPool pool;
private final UserRepository repo;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public RefreshAheadService(UserRepository repo){
this.repo = repo;
this.pool = RedisClientFactory.getPool();
// 示例:针对 user:123 进行提前刷新
scheduler.scheduleAtFixedRate(() -> refreshIfNeeded(123L), 0, 10, TimeUnit.SECONDS);
}
private void refreshIfNeeded(Long id){
String key = "user:" + id;
try (Jedis jedis = pool.getResource()){
Long ttl = jedis.ttl(key);
if (ttl == -2) return; // key 不存在
if (ttl >= 0 && ttl < 30) { // TTL 小于 30s 就刷新
User user = repo.findById(id);
if (user != null) jedis.setex(key, 300, JsonUtil.toJson(user));
}
}
}
}
要点:需要有“热点数据识别”机制,避免无用刷新造成 DB 压力。常结合访问统计或频率采样决定哪些 key 要 refresh-ahead。
7) 生产建议(工程细节与注意事项)
- 序列化/压缩:实体 JSON 体积大时考虑二进制序列化(Protobuffers)或压缩。
- 失效 & 一致性:选策略前先明确业务一致性需求(强一致/最终一致/可丢失)。
- 持久化写队列:Write-Back 必须用持久化队列(Kafka/Redis Stream/RabbitMQ)避免内存丢失。
- 分布式锁:对于缓存重建竞态(缓存击穿),使用互斥锁或“互斥删除+单例回源”模式。
- 缓存雪崩防护:使用随机 TTL(TTL jitter)、热点提前刷新、限流或降级策略。
- 监控与报警:缓存命中率、延迟、写队列长度、Redis 主从延迟等都要监控。
- 幂等与重试:落库保证幂等写,后台 flush 要能重试或持久化失败任务。