Java + Redis:缓存读写策略实战代码示例(工程可直接参考)

内容纲要

标签: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 要能重试或持久化失败任务。

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward