Redis缓存穿透解决方案

本文介绍了Redis缓存穿透问题及其危害,提供两种解决方案:1) 对于不存在的key设置默认值并设置过期时间,避免数据库压力;2) 使用布隆过滤器预先过滤无效请求,减少无效数据库查询。通过这两种方法,可以有效防止恶意攻击或误操作导致的数据库冲击。

Redis缓存穿透解决方案

1、什么是缓存穿透

查询的key在redis中不存在,对应的ID在数据库也不存在,此时被非法用户进行攻击,大量的请求会直接冲入数据库上造成宕机,从而影响整个系统,这种现象称之为:缓存穿透

@GetMapping("/findCategory/{cid}")
    public List<Category> findCategory(@PathVariable("cid") Integer cid) {
        // 判断分类id是否传入,如果没有传入那么直接返回
        KAssert.isEmpty(cid, 401, "分类不存在");
        List<Category> categoryList = new ArrayList<>();
        // 先去缓存中去获取分类信息
        String categories = (String) redisTemplate.opsForValue().get("subCid:" + cid);
        if (StringUtils.isEmpty(categories)) {
            log.info("数据库查询");
            categoryList = categoryService.findCategroies(cid);
            if (!CollectionUtils.isEmpty(categoryList)) {
                redisTemplate.opsForValue().set("subCid:" + cid, JsonUtil.obj2String(categoryList));
            }
        } else {
            categoryList = JsonUtil.string2Obj(categories, List.class, Category.class);
        }
        return categoryList;
    }

以上的代码是在开发中非常常见的代码。

但是如果有一些黑客或者肉机,故意输入一些不存在的ID,或者大量的用户输错了地址,那么缓存永远都是null。永远都命中到数据库。如果这个时候出现的请求并发很大,可能就会把数据库冲垮。

比如:cid==666 在数据表中不存在

String categories = (String) redisTemplate.opsForValue().get("subCid:" + cid);

上面的缓存代码为空,就会进入到数据db去查询,如果黑客或者肉机,大量错误输入,请求非常大的话就造成缓存穿透。

2、解决方案

2.1 将不存在的key,存一个默认值:null ,并且给予过期时间。
@GetMapping("/findCategory/{cid}")
public List<Category> findCategory(@PathVariable("cid") Integer cid) {
    // 判断分类id是否传入,如果没有传入那么直接返回
    KAssert.isEmpty(cid, 401, "分类不存在");
    List<Category> categoryList = new ArrayList<>();
    // 先去缓存中去获取分类信息
    String categories = (String) redisTemplate.opsForValue().get("subCid:" + cid);
    if (StringUtils.isEmpty(categories)) {
        log.info("数据库查询");
        // 如果不存在就从数据库查询
        categoryList = categoryService.findCategroies(cid);
        // 判断如果集合有数据,那么就会放入缓存。就是这个判断,造成了缓存穿透。
        if (CollectionUtils.isEmpty(categoryList)) {
            // 将不存在的key,存一个默认值:null,并且给予过期时间
            redisTemplate.opsForValue().set("subCid:" + cid, JsonUtil.obj2String(categoryList), 5 * 60, TimeUnit.SECONDS);
        } else {
            // 放入到redis缓存中
            redisTemplate.opsForValue().set("subCid:" + cid, JsonUtil.obj2String(categoryList));
        }
    } else {
        // 是否存在,如果存在直接从缓存返回
        categoryList = JsonUtil.string2Obj(categories, List.class, Category.class);
    }
    return categoryList;
}
2.2 布隆过滤器
@GetMapping("/findCategory/{cid}")
    public List<Category> findCategory(@PathVariable("cid") Integer cid) {
        // 判断分类id是否传入,如果没有传入那么直接返回
        KAssert.isEmpty(cid, 401, "分类不存在");
//         // 进行布隆过滤 bf.madd redis:bloom:category “1” "2" "3" "4" "5"
//         // 进行布隆过滤 bf.mexists redis:bloom:category “1”
        boolean[] booleans = reBloomClient.existsMulti("redis:bloom:category", cid + "");
//         // 如果cid没有在布隆过滤器中,说明你数据不存在,直接返回
        if (!booleans[0]) {
            throw new ValidationException(401, "分类不存在");
        }

        // BloomFilter的认识
        // 用了Redis缓存就真的不会进入数据库了吗?
        List<Category> categoryList = new ArrayList<>();
        // 先去缓存中去获取分类信息
        String categories = (String) redisTemplate.opsForValue().get("subCid:" + cid);
        if (StringUtils.isEmpty(categories)) {
            log.info("数据库查询");
            categoryList = categoryService.findCategroies(cid);
            if (!CollectionUtils.isEmpty(categoryList)) {
                redisTemplate.opsForValue().set("subCid:" + cid, JsonUtil.obj2String(categoryList));
            }
        } else {
            categoryList = JsonUtil.string2Obj(categories, List.class, Category.class);
        }
        return categoryList;
    }
### Redis缓存穿透解决方案:布隆过滤器 vs 缓存空值 #### 布隆过滤器 布隆过滤器是一种空间效率高的概率型数据结构,用于测试某个元素是否属于一个集合。其主要特点是能够快速判断某项是否存在,但可能会存在一定的假阳性率。 - **优点** - 节省内存:相比直接存储所有键值对,布隆过滤器仅需少量位来表示大量数据的存在状态[^2]。 - 查询速度快:由于基于哈希函数计算得出结果,查询时间复杂度接近 O(1)[^2]。 - **缺点** - 存在假阳性风险:即使实际不存在某些 key,在布隆过滤器中也可能被判定为存在[^2]。 - 不支持删除操作:一旦向布隆过滤器加入了一个元素就无法移除它而不影响其他成员的状态。 ```python import pybloom_live # 创建容量为10万错误率为0.1%的布隆过滤器实例 bf = pybloom_live.BloomFilter(capacity=1e5, error_rate=0.001) def check_key(key): if key not in bf: # 如果key不在布隆过滤器里,则肯定不会存在于真实数据源中 return False else: # 可能存在false positive的情况 return True or query_database_for_confirmation(key) ``` #### 缓存空值(或缺省值) 当发现请求的是一个不存在于数据库中的记录时,可以将其对应的 NULL 或者特定标志作为值放入到 redis 中并设定较短的有效期 (TTL),从而避免后续对该相同缺失资源发起重复查找动作直到该条目过期为止。 - **优点** - 实现简便易行:只需简单的逻辑修改即可完成部署实施过程[^1]。 - 维护成本低:无需引入额外的技术栈或者库文件依赖关系就可以达到目的效果[^1]。 - **缺点** - 占用更多内存资源:因为即使是那些本来就不应该存在的 keys 也会占用一定量的空间直至它们各自的 TTL 到期消失之前。 - 数据短暂不一致现象可能发生:假如在此期间原始数据发生了变化而未及时同步更新至缓存层的话那么这段时间内的响应将是陈旧甚至完全错误的信息[^1]。 ```python redis_client.set('nonexistent_key', 'null_value', ex=60) # 设置有效期为60秒 value = redis_client.get('some_key') if value is None: db_result = fetch_from_db('some_key') # 尝试从数据库获取 if db_result is None: redis_client.set('some_key', 'null_value', ex=60) # 缓存空值以防下次再查 else: process(value.decode()) ``` #### 如何选择合适的方案? 对于具体应用场景而言: - 当系统的可用性和精确性要求非常高,并且不允许任何程度上的误判发生时应优先考虑采用布隆过滤器方式; - 若项目开发周期紧张或是团队技术水平有限难以驾驭复杂的算法模型则可以选择相对容易理解和维护同时也较为有效的缓存空对象策略;当然也可以两者结合起来使用即先通过布隆过滤初步筛查然后再针对可疑样本做进一步验证最后决定是否真的需要把他们放进真正的高速缓冲区里面去保存起来供以后调用之便[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫拾壹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值