【Redis】关于如何解决缓存穿透

1. 什么是缓存穿透?

    缓存穿透是指查询一个不存在的数据,由于缓存不会保存这样的查询结果,因此每次都会直接查询数据库。如果这种不存在的查询请求频繁发生,会给数据库带来不必要的压力,甚至可能导致数据库崩溃。

   例如,一个电商系统中,用户请求查询一个不存在的商品信息(如商品ID为123456),系统会先去缓存中查找。如果缓存中没有,就会查询数据库。但如果数据库中也不存在这个商品,那么后续类似的请求都会直接打到数据库,从而引发性能问题。

2. 解决缓存穿透的方法

(1)接口层面校验

在查询之前,先对请求的参数进行合法性校验。如果参数明显不合理(如商品ID为负数、格式不正确等),直接返回错误,避免进行后续的缓存和数据库查询。

优点:简单高效,能从源头上避免无效请求。

缺点:无法完全解决恶意请求(如恶意构造不存在的合理参数)。

public Object getItemById(Long id) {
    if (id == null || id <= 0) {
        throw new IllegalArgumentException("Invalid ID: " + id);
    }

    // 从缓存获取数据
    Object item = redisTemplate.opsForValue().get("item:" + id);
    if (item != null) {
        return item;
    }

    // 缓存未命中,查询数据库
    item = database.getItemById(id);
    if (item != null) {
        // 将数据存入缓存
        redisTemplate.opsForValue().set("item:" + id, item, 60, TimeUnit.SECONDS);
    }

    return item;
}
(2)缓存空对象

当数据库中没有查询到数据时,将一个空对象(如null或自定义的空对象)存入缓存,并设置一个较短的过期时间(如5分钟)。这样,后续相同的请求可以直接从缓存中获取空对象,而不会再次查询数据库。

优点:能有效减轻数据库压力。

缺点:缓存中会存在一些无用的空对象,占用一定的内存空间。

public Object getItemById(Long id) {
    if (id == null || id <= 0) {
        throw new IllegalArgumentException("Invalid ID: " + id);
    }

    // 从缓存获取数据
    Object item = redisTemplate.opsForValue().get("item:" + id);
    if (item != null) {
        return item;
    }

    // 缓存未命中,查询数据库
    item = database.getItemById(id);
    if (item == null) {
        // 数据库中不存在,缓存空对象
        redisTemplate.opsForValue().set("item:" + id, "null", 5, TimeUnit.MINUTES);
        return null;
    }

    // 将数据存入缓存
    redisTemplate.opsForValue().set("item:" + id, item, 60, TimeUnit.SECONDS);
    return item;
}
(3)布隆过滤器(Bloom Filter)

布隆过滤器是一种高效的空间数据结构,用于判断一个元素是否在一个集合中。在应用启动时,将所有可能存在的数据ID(如商品ID)存入布隆过滤器。当查询时,先通过布隆过滤器判断该ID是否存在:

  • 如果布隆过滤器返回不存在,则直接返回错误,不查询缓存和数据库;

  • 如果布隆过滤器返回可能存在,则继续查询缓存和数据库。

优点:高效、节省内存空间。

缺点:布隆过滤器存在一定的误判率(如将不存在的ID误判为存在),需要根据实际场景选择合适的误判率。

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterExample {
    private static final int EXPECTED_INSERTIONS = 1000000; // 预期插入的元素数量
    private static final double FPP = 0.001; // 误判率

    private BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), EXPECTED_INSERTIONS, FPP);

    public BloomFilterExample() {
        // 初始化布隆过滤器,将所有可能存在的数据 ID 加入
        List<String> allItemIds = database.getAllItemIds();
        for (String id : allItemIds) {
            bloomFilter.put(id);
        }
    }

    public boolean mightContain(String id) {
        return bloomFilter.mightContain(id);
    }
}
(4)分布式锁

如果缓存穿透问题是由高并发的首次查询导致的(如多个线程同时查询一个不存在的数据),可以使用分布式锁(如Redisson)来控制并发。当第一个线程查询数据库并写入缓存后,其他线程可以直接从缓存中获取结果。

优点:能有效解决高并发场景下的缓存穿透问题。

缺点:实现相对复杂,需要引入分布式锁组件。

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonLockExample {
    private RedissonClient redissonClient;

    public RedissonLockExample() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
    }

    public Object getItemById(Long id) {
        if (id == null || id <= 0) {
            throw new IllegalArgumentException("Invalid ID: " + id);
        }

        // 从缓存获取数据
        Object item = redisTemplate.opsForValue().get("item:" + id);
        if (item != null) {
            return item;
        }

        // 获取分布式锁
        RLock lock = redissonClient.getLock("itemLock:" + id);
        try {
            lock.lock();
            // 再次检查缓存
            item = redisTemplate.opsForValue().get("item:" + id);
            if (item != null) {
                return item;
            }

            // 查询数据库
            item = database.getItemById(id);
            if (item == null) {
                // 数据库中不存在,缓存空对象
                redisTemplate.opsForValue().set("item:" + id, "null", 5, TimeUnit.MINUTES);
                return null;
            }

            // 将数据存入缓存
            redisTemplate.opsForValue().set("item:" + id, item, 60, TimeUnit.SECONDS);
        } finally {
            lock.unlock();
        }

        return item;
    }
}

通常会结合多种方法来解决缓存穿透问题:

  1. 基础手段接口层面校验是必须的,可以过滤掉大部分非法请求。

  2. 核心手段缓存空对象是性价比最高的方案,适用于大多数场景。

  3. 高级手段:如果数据量较大且对性能要求较高,可以结合布隆过滤器

  4. 高并发场景:如果系统并发量较高,可以结合分布式锁来避免缓存穿透问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值