解决Redis缓存穿透(缓存空对象、布隆过滤器)

背景

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库

常见的解决方案有两种,分别是缓存空对象布隆过滤器

1.缓存空对象

image-20241025163728328

优点:实现简单,维护方便

缺点:额外的内存消耗、可能造成短期的不一致

2.布隆过滤器

image-20241025163737389

优点:内存占用较少,没有多余key

缺点:实现复杂、存在误判可能

代码实现

前置

这里以根据 id 查询商品店铺为案例

实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {
   
   

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 商铺名称
     */
    private String name;

    /**
     * 商铺类型的id
     */
    private Long typeId;

    /**
     * 商铺图片,多个图片以','隔开
     */
    private String images;

    /**
     * 商圈,例如陆家嘴
     */
    private String area;

    /**
     * 地址
     */
    private String address;

    /**
     * 经度
     */
    private Double x;

    /**
     * 维度
     */
    private Double y;

    /**
     * 均价,取整数
     */
    private Long avgPrice;

    /**
     * 销量
     */
    private Integer sold;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 评分,1~5分,乘10保存,避免小数
     */
    private Integer score;

    /**
     * 营业时间,例如 10:00-22:00
     */
    private String openHours;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


    @TableField(exist = false)
    private Double distance;
}

常量类

public class RedisConstants {
   
   
    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";
}

工具类

public class ObjectMapUtils {
   
   

    // 将对象转为 Map
    public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
   
   
        Map<String, String> result = new HashMap<>();
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
   
   
            // 如果为 static 且 final 则跳过
            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
   
   
                continue;
            }
            field.setAccessible(true); // 设置为可访问私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
   
   
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 将 Map 转为对象
    public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {
   
   
        Object obj = clazz.getDeclaredConstructor().newInstance
<think>嗯,用户问的是Redis缓存穿透,以及缓存对象是否属于布隆过滤机制。首先我需要确认缓存穿透的定义,然后解释缓存对象布隆过滤器的区别和联系。 缓存穿透指的是查询一个不存在的数据,导致每次请求都绕过缓存直接访问数据库,这可能会压垮数据库。常见的解决办法有两种:缓存对象布隆过滤器。用户可能混淆了这两种方法,或者想了解它们之间的关系。 缓存对象是指在查询数据库未找到数据时,将结果缓存起来,并设置较短的过期时间,这样后续的请求会命中缓存,避免直接访问数据库。而布隆过滤器是一种概率型数据结构,用来快速判断一个元素是否存在于集合中,可以预先将所有可能存在的数据哈希到布隆过滤器中,查询时如果布隆过滤器认为不存在,就直接返回,避免查询数据库。 用户可能误解缓存对象布隆过滤器的一种,但实际上它们是不同的解决方案。我需要明确区分两者的原理、优缺点以及适用场景,并举例说明如何结合使用来增强系统性能。此外,用户可能在实际应用中遇到了相关问题,需要具体的配置示例或代码片段,因此提供实际案例会更有帮助。最后,需要总结两者的区别,并给出最佳实践建议,帮助用户根据具体情况选择合适的方法。</think>--- ### 1. **核心结论** **缓存穿透**和**布隆过滤器**是不同概念,**缓存对象**与**布隆过滤器**是解决缓存穿透的两种不同方案。两者核心区别如下: - **缓存对象**:直接缓存结果,但需设置较短过期时间(如30秒)。 - **布隆过滤器**:通过概率性数据结构提前过滤无效请求,避免缓存层和数据库层同时被穿透。 --- ### 2. **缓存穿透的定义与风险** #### **什么是缓存穿透?** - **场景**:请求查询一个**数据库中不存在的数据**,导致缓存未命中(Cache Miss),请求直接穿透到数据库。 - **风险**: - 高频无效查询压垮数据库。 - 攻击者可能利用此漏洞发起恶意请求(如随机ID探测)。 #### **示例** ```plaintext 请求:查询用户ID=10000的信息(数据库中不存在此ID) 流程:Redis缓存 → 查询数据库 → 数据库返回 → 下次请求继续重复此过程 ``` --- ### 3. **缓存对象方案** #### **原理** - **缓存结果**:即使数据库返回值,仍将结果(如`null`或特殊标记)写入缓存,并设置较短的过期时间(例如5-30秒)。 - **后续请求直接命中缓存**:避免重复穿透到数据库。 #### **实现示例** ```java public User getUserById(String id) { // 1. 先查缓存 User user = redis.get(id); if (user != null) { if (user.isEmptyObject()) { // 判断是否为对象标记 return null; // 直接返回,避免查库 } return user; } // 2. 缓存未命中,查数据库 user = db.query("SELECT * FROM users WHERE id = ?", id); if (user == null) { // 缓存对象,过期时间30秒 redis.setex(id, 30, "empty"); // 使用特殊标记(如"empty") return null; } else { redis.setex(id, 3600, user); // 缓存真实数据 return user; } } ``` #### **优缺点** | 优点 | 缺点 | |------|------| | 实现简单 | 可能缓存大量无效Key,占用内存 | | 对代码侵入性低 | 恶意攻击者可能构造不同Key耗尽内存 | | 短期缓解穿透问题 | 数据一致性需处理(如新用户注册后需清理标记) | --- ### 4. **布隆过滤器方案** #### **原理** - **预加载有效数据**:将所有**可能存在的数据**的Key哈希到布隆过滤器中。 - **请求过滤**:查询前先通过布隆过滤器判断Key是否存在: - **若不存在**:直接返回,避免访问缓存和数据库。 - **若存在**:继续查询缓存/数据库。 #### **实现示例** ```java // 初始化布隆过滤器(例如启动时加载数据库所有有效用户ID) BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.01); List<String> allUserIds = db.queryAllUserIds(); bloomFilter.putAll(allUserIds); public User getUserById(String id) { // 1. 先查布隆过滤器 if (!bloomFilter.mightContain(id)) { return null; // 直接返回,避免后续查询 } // 2. 查缓存 User user = redis.get(id); if (user != null) { return user; } // 3. 查数据库并回填缓存 user = db.query("SELECT * FROM users WHERE id = ?", id); if (user != null) { redis.setex(id, 3600, user); } return user; } ``` #### **优缺点** | 优点 | 缺点 | |------|------| | 内存占用低(基于位数组) | 存在误判率(可能将有效Key误判为不存在) | | 彻底避免无效请求穿透到存储层 | 需提前预热数据,不适合动态频繁更新的场景 | | 适合固定数据集(如商品ID列表) | 无法删除已添加的Key(需结合计数布隆过滤器) | --- ### 5. **两种方案对比** | 维度 | 缓存对象 | 布隆过滤器 | |------|------------|------------| | **内存占用** | 可能较高(存储大量值) | 较低(位数组存储) | | **数据一致性** | 需主动清理标记 | 需定期同步布隆过滤器数据 | | **适用场景** | Key间有限或动态变化 | Key间固定或低频更新 | | **防御能力** | 仅缓解短期穿透 | 彻底拦截无效请求 | --- ### 6. **组合使用方案** 实际项目中可结合两种方案: 1. **布隆过滤器**拦截绝大多数无效请求。 2. **缓存对象**处理布隆过滤器误判的请求(即布隆过滤器认为可能存在但实际不存在的Key)。 3. **定期更新布隆过滤器**以同步最新数据(如每天全量同步一次)。 --- ### 7. **其他优化措施** - **限流熔断**:对高频无效请求的客户端实施限流(如Nginx或Sentinel)。 - **异步预热**:热点数据提前加载到缓存。 - **动态布隆过滤器**:使用可删除的布隆过滤器变种(如Cuckoo Filter)。 --- ### 8. **总结** - **缓存对象**是“事后处理”,通过存储值减少穿透。 - **布隆过滤器**是“事前拦截”,通过预判过滤无效请求。 - **选择建议**: - 若数据ID范围有限且变动少(如商品ID),优先用布隆过滤器。 - 若数据动态性强或无法预加载,用缓存对象+短期过期策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值