当前位置: 首页 > article >正文

java-redis-穿透

Redis 缓存穿透是指当请求的数据在缓存和数据库中都不存在时,用户每次请求都会直接查询数据库,导致缓存失效,无法发挥作用。这种情况下,用户发出的每个请求都绕过了缓存,直接打到了数据库,可能导致数据库压力骤增,甚至崩溃。

在实际应用中,缓存穿透通常会由于用户发送恶意请求或非法数据请求导致。例如,用户传递的 id 永远不会在数据库中找到相应的数据(如负数 ID 或过大的 ID)。由于这些 ID 不存在于数据库中,缓存也没有保存这些值,结果是每次请求都会直接访问数据库。

Redis 缓存穿透的常见解决方案

  1. 缓存空值:将不存在的结果也缓存起来,避免重复查询数据库。
  2. 布隆过滤器:通过布隆过滤器提前拦截非法请求,避免查询数据库。
  3. 参数校验:在查询数据库之前,先对请求参数进行校验,直接过滤掉非法请求。

接下来,我们逐一解释这些方案,并给出示例代码。

1. 缓存空值

当请求的键在数据库中不存在时,我们可以将这个“空结果”也缓存起来,并设置一个较短的过期时间,避免缓存永远存储无效值。下次再请求这个键时,直接从缓存中获取到空值,而不会访问数据库。

示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 模拟查询数据库
     */
    public String getDataFromDB(String key) {
        // 模拟数据库查询
        if ("validKey".equals(key)) {
            return "valueFromDB";
        }
        return null; // 模拟数据库中不存在
    }

    /**
     * 查询数据,避免缓存穿透
     */
    public String getData(String key) {
        // 先从缓存中查询
        String value = redisTemplate.opsForValue().get(key);
        
        if (value != null) {
            // 缓存命中
            return "从缓存获取: " + value;
        }

        // 如果缓存没有命中,则查询数据库
        value = getDataFromDB(key);

        if (value != null) {
            // 如果数据库存在,则写入缓存
            redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
            return "从数据库获取: " + value;
        } else {
            // 如果数据库不存在,则将空值缓存,并设置短期过期时间
            redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
            return "数据不存在,缓存空值";
        }
    }
}

在这个例子中,当数据库中找不到数据时,缓存会存储一个空字符串,防止后续的重复查询。可以根据实际场景设置空值的过期时间,例如设置为 5 分钟。这样可以减少数据库的压力。

2. 使用布隆过滤器

布隆过滤器(Bloom Filter)是一种高效的概率数据结构,用于快速判断某个元素是否存在于一个集合中。通过将所有合法的键加入布隆过滤器中,可以在查询数据库前先判断请求是否有意义,如果布隆过滤器认为该键不存在,则可以直接返回,不再查询缓存和数据库。

布隆过滤器的特点:

  • 空间效率高:布隆过滤器使用哈希函数和位数组来表示元素的存在状态,空间占用很小。
  • 存在误判:布隆过滤器可能会误判某个不存在的元素为存在(即假阳性),但不会误判存在的元素为不存在。
示例代码:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

@Service
public class CacheServiceWithBloomFilter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private BloomFilter<String> bloomFilter;

    // 初始化布隆过滤器
    public CacheServiceWithBloomFilter() {
        // 创建布隆过滤器,设置预计插入元素数量为10000,误判率为0.01
        this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 10000, 0.01);
        
        // 将已有数据添加到布隆过滤器
        bloomFilter.put("validKey");
        // 可以继续添加其他已知合法的键
    }

    /**
     * 模拟查询数据库
     */
    public String getDataFromDB(String key) {
        if ("validKey".equals(key)) {
            return "valueFromDB";
        }
        return null;
    }

    /**
     * 查询数据,使用布隆过滤器避免缓存穿透
     */
    public String getData(String key) {
        // 先通过布隆过滤器判断该key是否可能存在
        if (!bloomFilter.mightContain(key)) {
            return "该数据不存在(布隆过滤器拦截)";
        }

        // 如果布隆过滤器认为存在,继续查询缓存
        String value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return "从缓存获取: " + value;
        }

        // 缓存未命中,查询数据库
        value = getDataFromDB(key);
        if (value != null) {
            // 数据库存在,写入缓存
            redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
            return "从数据库获取: " + value;
        } else {
            // 数据库不存在,缓存空值
            redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
            return "数据不存在,缓存空值";
        }
    }
}

在这个例子中,布隆过滤器用于快速判断请求的键是否可能存在。如果布隆过滤器判断该键不可能存在,则直接返回“数据不存在”,避免不必要的缓存和数据库查询。

3. 参数校验

对于明显不合法的请求参数(例如负数 ID、过大的 ID 等),可以在请求到达 Redis 或数据库之前直接进行参数校验,避免不合法的请求进入后端系统。

示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class CacheServiceWithValidation {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 模拟查询数据库
     */
    public String getDataFromDB(String key) {
        if ("validKey".equals(key)) {
            return "valueFromDB";
        }
        return null;
    }

    /**
     * 查询数据,增加参数校验
     */
    public String getData(String key) {
        // 进行参数合法性校验,过滤掉明显不合法的请求
        if (key == null || key.length() == 0 || key.length() > 20) {
            return "请求参数不合法";
        }

        // 查询缓存
        String value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return "从缓存获取: " + value;
        }

        // 缓存未命中,查询数据库
        value = getDataFromDB(key);
        if (value != null) {
            redisTemplate.opsForValue().set(key, value, 10, TimeUnit.MINUTES);
            return "从数据库获取: " + value;
        } else {
            redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
            return "数据不存在,缓存空值";
        }
    }
}

在这个例子中,首先对请求的 key 进行参数校验,确保请求是合法的。如果 key 为空、长度过长等情况,直接返回错误信息,避免查询缓存和数据库。

Redis 穿透、击穿和雪崩

除了缓存穿透,还有两个常见的缓存问题:

  • 缓存击穿:缓存中没有但数据库中有的数据,并且该数据在短时间内有大量请求。如果这类请求同时访问数据库,可能导致数据库压力激增。

    解决方案:使用互斥锁(如分布式锁)防止多个线程同时查询数据库,或者为热点数据设置较长的过期时间。

  • 缓存雪崩:由于缓存服务器宕机或大量缓存同时失效,导致大量请求直接打到数据库,给数据库造成很大压力。

    解决方案:为缓存设置不同的过期时间(缓存失效时间的随机化),避免缓存集中失效;增加缓存节点的冗余。

总结

缓存穿透是 Redis 缓存系统中的一个常见问题,它会导致大量请求绕过缓存直接访问数据库,给数据库带来巨大的压力。针对缓存穿透,我们可以使用以下方案:

  1. **缓存空值

**:将数据库中不存在的值缓存起来,防止重复查询。
2. 布隆过滤器:在查询缓存和数据库之前,利用布隆过滤器判断请求是否合法,从而减少不必要的数据库查询。
3. 参数校验:在应用层进行参数合法性校验,直接拦截明显非法的请求。


http://www.kler.cn/a/301302.html

相关文章:

  • Flutter在MaterialApp中的builder初始化多个包
  • 【含开题报告+文档+PPT+源码】基于springboot的教师评价系统的设计与实现
  • delphi fmx android 离线人脸识别
  • 近几年新笔记本重装系统方法及一些注意事项
  • 图像基础算法学习笔记
  • 基于YOLOv8深度学习的智慧课堂学生专注度检测系统(PyQt5界面+数据集+训练代码)
  • .NET 一款支持NTLM实现横向移动的工具
  • Python实现模糊逻辑算法
  • 今年白银市场的供需关系矛盾
  • Java教程:SE进阶【十万字详解】(上)
  • Android Environment 获取的路径问题
  • MySQL灾难恢复策略:构建稳健的备份与恢复机制
  • vue3项目npm i安装依赖一直转圈或安装失败解决方法~
  • CCF刷题计划——矩阵运算(同时转置+乘法)
  • 深度学习驱动的车牌识别:技术演进与未来挑战
  • 主窗口的设计与开发(二)
  • 3、三维重建-NeuralRecon
  • 东莞网站制作-如何优化推广
  • web框架
  • 【linux】一种基于虚拟串口的方式使两个应用通讯
  • 使用kubeadm手动安装K8s
  • C++学习笔记----6、内存管理(五)---- 智能指针(4)
  • 使用patch命令移除sts中的一个container
  • 【CTF Web】BUUCTF Upload-Labs-Linux Pass-13 Writeup(文件上传+PHP+文件包含漏洞+JPEG图片马)
  • 力扣100题——动态规划
  • 【MATLAB】数据和字符串类型转换