Java缓存结构案例怎么规整

wen java案例 33

本文目录导读:

Java缓存结构案例怎么规整

  1. 缓存结构设计的核心原则
  2. 案例 1:标准分层缓存结构(最推荐)
  3. 案例 2:注解驱动缓存结构(Spring Cache风格)
  4. 案例 3:针对特定场景的规整结构
  5. 通用工程化和可视化监控
  6. 规整化总结表

针对Java缓存结构的规整化设计,这里整理了一套从简单到复杂的分层设计案例,涵盖了常见场景下的最佳实践和代码结构。

缓存结构设计的核心原则

在写代码之前,先明确要遵循的架构思路:

  1. 分层原则: L1(本地) -> L2(分布式)-> DB
  2. 单一职责: 缓存管理类只负责缓存逻辑,不混入业务代码
  3. 可替换性: 底层实现(Redis, Caffeine, Map)可配置切换
  4. 防御式设计: 缓存穿透、击穿、雪崩的处理

案例 1:标准分层缓存结构(最推荐)

这是最常见的多级缓存结构,适用于用户信息、商品详情等高频访问场景。

项目包结构

com.example.cache
├── config
│   └── CacheConfig.java                // 缓存配置(过期时间、大小)
├── multiLevel
│   ├── CacheData.java                  // 缓存数据包装类
│   ├── CacheLoader.java                // 缓存加载器接口
│   └── MultiLevelCache.java            // 多级缓存实现
├── service
│   └── UserService.java                // 业务服务(使用缓存)
└── common
    └── CacheNotFoundException.java     // 自定义异常

核心实现代码

① 缓存数据包装类

// CacheData.java - 封装缓存值和加载来源
@Data
@AllArgsConstructor
public class CacheData<T> {
    private T value;
    private String source; // L1/L2/DB
    private long loadTime; // 加载时间戳,用于判断是否过期
}

② 多级缓存管理器

// MultiLevelCache.java - 核心多级缓存实现
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
public class MultiLevelCache<K, V> {
    // L1: 本地缓存 (Caffeine) - 极快,容量小
    private final Cache<K, V> localCache;
    // L2: 分布式缓存 (Redis) - 较快,容量大
    private final Jedis redisClient;
    // L3: 数据库加载器
    private final CacheLoader<K, V> dbLoader;
    // 统计指标(可选)
    private final AtomicLong l1Hits = new AtomicLong(0);
    private final AtomicLong l2Hits = new AtomicLong(0);
    public MultiLevelCache(Jedis redisClient, CacheLoader<K, V> dbLoader) {
        this.redisClient = redisClient;
        this.dbLoader = dbLoader;
        // 初始化L1本地缓存:最大1000条,过期5分钟
        this.localCache = Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .recordStats() // 开启统计
                .build();
    }
    @SuppressWarnings("unchecked")
    public V get(K key) {
        // 1. 尝试L1本地缓存
        V value = localCache.getIfPresent(key);
        if (value != null) {
            l1Hits.incrementAndGet();
            return value;
        }
        // 2. 尝试L2 Redis缓存(序列化后存储)
        String redisValue = redisClient.get(key.toString());
        if (redisValue != null) {
            l2Hits.incrementAndGet();
            // 反序列化
            value = deserialize(redisValue);
            // 回填L1缓存
            localCache.put(key, value);
            return value;
        }
        // 3. 回源DB查询
        value = dbLoader.load(key);
        if (value != null) {
            // 同时回填L1和L2
            localCache.put(key, value);
            redisClient.setex(key.toString(), 3600, serialize(value)); // 1小时过期
        }
        return value;
    }
    // 包含防御措施的带锁版本(防击穿)
    public V getWithLock(K key) {
        // 校验空值缓存(防穿透)
        V value = localCache.getIfPresent(key);
        if (value != null) {
            return value instanceof NullValueCache ? null : value;
        }
        // 分布式锁(防击穿)
        String lockKey = "LOCK:" + key;
        if (acquireLock(lockKey)) {
            try {
                // 双重检查
                V v = localCache.getIfPresent(key);
                if (v != null) return v;
                // 查DB
                value = dbLoader.load(key);
                // 缓存空值对象(防穿透)
                localCache.put(key, value != null ? value : new NullValueCache());
                if (value != null) {
                    redisClient.setex(key.toString(), 3600, serialize(value));
                } else {
                    redisClient.setex(key.toString(), 60, ""); // 短暂空值缓存
                }
            } finally {
                releaseLock(lockKey);
            }
        }
        return value;
    }
    public void invalidate(K key) {
        localCache.invalidate(key);
        redisClient.del(key.toString());
    }
}

③ 缓存加载器接口

// CacheLoader.java - 让业务方实现DB加载逻辑
@FunctionalInterface
public interface CacheLoader<K, V> {
    V load(K key);
}

④ 防穿透的空值对象

// NullValueCache.java - 用于缓存null防止穿透
public class NullValueCache implements Serializable {
    // 标记类,不包含实际数据
}

业务使用示例

// UserService.java
@Service
public class UserService {
    private final MultiLevelCache<String, User> userCache;
    public UserService() {
        // 构造时注入DB加载器
        userCache = new MultiLevelCache<>(redisClient, userId -> {
            // 这是L3:真正查询数据库
            return userMapper.selectById(userId);
        });
    }
    public User getUser(String userId) {
        return userCache.getWithLock("USER:" + userId);
    }
    public void updateUser(String userId, User user) {
        userMapper.update(user);
        userCache.invalidate("USER:" + userId); // 主动失效
    }
}

案例 2:注解驱动缓存结构(Spring Cache风格)

如果你用Spring框架,可以封装自己的缓存注解,让代码更简洁。

自定义注解

// MultiLevelCacheable.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiLevelCacheable {
    String key();                // 缓存键
    int ttlSeconds() default 600; // 过期时间
    boolean useLocalCache() default true; // 是否用L1
}

注解解析AOP

// CacheAspect.java
@Aspect
@Component
public class CacheAspect {
    private final MultiLevelCache<String, Object> multiLevelCache;
    @Around("@annotation(cacheable)")
    public Object cacheAround(ProceedingJoinPoint pjp, MultiLevelCacheable cacheable) {
        String key = resolveKey(cacheable.key(), pjp.getArgs());
        // 尝试从缓存获取
        Object cachedValue = multiLevelCache.get(key);
        if (cachedValue != null) {
            return cachedValue;
        }
        // 执行原方法(即DB查询)
        Object result = pjp.proceed();
        // 回填缓存
        multiLevelCache.put(key, result, cacheable.ttlSeconds());
        return result;
    }
    private String resolveKey(String keyTemplate, Object[] args) {
        // 解析key中的占位符,如 #userId
        // 实际使用Spring Expression Language (SpEL)
        return keyTemplate;
    }
}

使用方式

@Service
public class ProductService {
    @MultiLevelCacheable(key = "'PRODUCT:' + #productId", ttlSeconds = 600)
    public Product getProduct(String productId) {
        return productMapper.selectById(productId);
    }
}

案例 3:针对特定场景的规整结构

场景:热点数据 + 频繁更新的计数器

缓存结构:

L1: Redis Hash (热点数据)
L2: Redis String (通用数据)
L3: MySQL (持久化)
// HotDataCache.java
public class HotDataCache {
    // 热更新工具:每5秒批量刷回DB
    @Scheduled(fixedRate = 5000)
    public void flushHotData() {
        // 将内存中的热数据增量刷到MySQL
    }
    // 热点判定:访问频率 > 阈值
    public boolean isHotKey(String key) {
        return accessCounter.get(key) > 100; // 每分钟100次
    }
}

通用工程化和可视化监控

缓存监控端点

// /actuator/cache-metrics 返回
{
  "multiLevelCache": {
    "l1HitRate": 0.85,
    "l2HitRate": 0.12,
    "totalRequests": 100000,
    "cacheSize": 980
  }
}

缓存管理控制台

提供一个简单的REST API来管理缓存:

  • POST /cache/invalidate:手动失效某个key
  • GET /cache/stats:查看命中率
  • POST /cache/preload:预热热门数据

规整化总结表

维度 推荐做法 避免的做法
层级 L1(Caffeine)+L2(Redis)+DB 单层缓存或全部本地
过期策略 本地短(5min)+Redis中(1h) 统一过期时间
穿透处理 缓存空值对象 + Bloom Filter 不处理或仅判断null
击穿处理 分布式锁 + 双重检查 无锁的回源
序列化 protobuf 或 JSON(定制) 使用默认Java序列化
代码结构 独立的CacheManager + 业务接口 在Service里直接new Cache
可观测性 暴露命中率、大小、加载耗时 不记录任何指标

最终建议:对于工程级项目,采用 案例1(手写多级缓存) 作为基座,配合 案例2(注解式) 提高开发效率,可以让缓存代码既规整又灵活。

抱歉,评论功能暂时关闭!