Java案例怎么实现缓存穿透?

wen java案例 16

本文目录导读:

Java案例怎么实现缓存穿透?

  1. 模拟缓存穿透(出现)
  2. 解决方案一:缓存空对象(Null Cache)
  3. 解决方案二:布隆过滤器(Bloom Filter)
  4. 关键点总结

缓存穿透是指查询一个根本不存在的数据,缓存层(如 Redis)和存储层(如数据库)都没有,导致每次请求都穿透到数据库,失去缓存保护。

面试或写代码时,实现的思路通常是“模拟”穿透,而不是“实现”穿透——那是系统设计要避免的。
既然你问“怎么实现”,可能是想写一个小 Demo 来演示这种场景,然后验证布隆过滤器或空值缓存等解决方案。

下面给出一个 Java 案例,演示缓存穿透的发生与解决


模拟缓存穿透(出现)

场景:
查用户信息,缓存用 Map 模拟(或 Redis),数据库用 List 模拟。
查一个不存在的 ID(-1),每次都会查库。

import java.util.HashMap;
import java.util.Map;
public class CachePenetrationDemo {
    // 模拟缓存
    private Map<String, String> cache = new HashMap<>();
    // 模拟数据库
    private Map<String, String> db = new HashMap<>();
    public CachePenetrationDemo() {
        db.put("1", "张三");
        db.put("2", "李四");
    }
    // 没有穿透防护
    public String getUserNoProtect(String id) {
        // 1. 查缓存
        String name = cache.get(id);
        if (name != null) {
            System.out.println("缓存命中: " + name);
            return name;
        }
        // 2. 查数据库
        System.out.println("缓存未命中,查询数据库...");
        name = db.get(id);
        if (name != null) {
            cache.put(id, name);  // 放入缓存
        }
        return name;
    }
    public static void main(String[] args) {
        CachePenetrationDemo demo = new CachePenetrationDemo();
        // 多次查询不存在的 id
        System.out.println("===== 缓存穿透演示 =====");
        System.out.println(demo.getUserNoProtect("-1"));  // 查库
        System.out.println(demo.getUserNoProtect("-1"));  // 仍然查库!因为没有缓存
        System.out.println(demo.getUserNoProtect("-1"));  // 每次都是库
    }
}

输出结果:

缓存未命中,查询数据库...
null
缓存未命中,查询数据库...
null
缓存未命中,查询数据库...
null

每次请求都落到数据库,这就是 缓存穿透


解决方案一:缓存空对象(Null Cache)

思路:查询不存在的数据时,缓存一个空值(或特殊标记),设置较短的过期时间。

// 防止穿透:缓存空对象
public String getUserWithNullCache(String id) {
    // 1. 查缓存
    String name = cache.get(id);
    if (name != null) {
        System.out.println("缓存命中: " + name);
        return "null".equals(name) ? null : name;
    }
    // 2. 查数据库
    System.out.println("缓存未命中,查询数据库...");
    name = db.get(id);
    if (name != null) {
        cache.put(id, name);          // 缓存正常值
    } else {
        cache.put(id, "null");        // 缓存空标记(过期时间短)
    }
    return name;
}

测试:

System.out.println("===== 空值缓存保护 =====");
System.out.println(demo.getUserWithNullCache("-1"));  // 查库,缓存 null
System.out.println(demo.getUserWithNullCache("-1"));  // 缓存命中,返回 null

输出(第二次不再查库):

缓存未命中,查询数据库...
null
缓存命中: null         ← 直接返回,不查库

解决方案二:布隆过滤器(Bloom Filter)

在查询前先判断是否存在,不存在则直接返回,存在才查缓存/库。

import java.util.BitSet;
// 简单的布隆过滤器实现(仅用于演示)
class BloomFilter {
    private BitSet bits = new BitSet(1 << 24);
    private int[] seeds = {3, 5, 7, 11, 13, 17};
    private int hash(String key, int seed) {
        int h = 0;
        for (char c : key.toCharArray()) {
            h = h * seed + c;
        }
        // 取模到 bits 大小
        return Math.abs(h % bits.size());
    }
    public void add(String key) {
        for (int seed : seeds) {
            bits.set(hash(key, seed));
        }
    }
    public boolean mightContain(String key) {
        for (int seed : seeds) {
            if (!bits.get(hash(key, seed))) {
                return false;   // 肯定不存在
            }
        }
        return true;            // 可能存在
    }
}
// 使用布隆过滤器保护
BloomFilter bloom = new BloomFilter();
// 初始化:把所有合法 ID 加入布隆过滤器
{
    bloom.add("1");
    bloom.add("2");
}
public String getUserWithBloom(String id) {
    // 1. 布隆过滤器快速判断
    if (!bloom.mightContain(id)) {
        System.out.println("布隆过滤器判定不存在,直接返回");
        return null;
    }
    // 2. 查缓存
    String name = cache.get(id);
    if (name != null) {
        System.out.println("缓存命中: " + name);
        return "null".equals(name) ? null : name;
    }
    // 3. 查库
    System.out.println("查数据库...");
    name = db.get(id);
    if (name != null) {
        cache.put(id, name);
    } else {
        cache.put(id, "null");
    }
    return name;
}

测试:

System.out.println("===== 布隆过滤器保护 =====");
System.out.println(demo.getUserWithBloom("-1"));  // 直接返回,不查库也不查缓存
System.out.println(demo.getUserWithBloom("1"));   // 正常查缓存

关键点总结

方案 原理 缺点 适合场景
缓存空对象 不存在的数据也缓存,设短过期时间 占用缓存空间,不一致窗口 穿透量不大
布隆过滤器 提前判断 key 是否存在 有误判率(会把存在判为不存在) 大量穿透攻击

如果你想要的是 纯“实现穿透”代码(即故意制造穿透),直接看第一个 getUserNoProtect 方法即可——它没有任何防护,每次不存在就查库,这就是缓存穿透的表现。

在真实项目中,你通常要做的是:认出它,然后加上空值缓存或布隆过滤器

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