缓存问题之缓存穿透

缓存穿透描述

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如用户伪造特别大的Id 值(数据库中不存在此Id),并且自增循环频繁请求接口,大量请求打到数据库上,导致数据库压力过大,从而导致服务器响应缓慢,影响用户体验。

缓存穿透理解

缓存穿透是指查询一个根本不存在的数据,缓存层和持久层都不会命中。在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护持久层的意义。

缓存穿透示意图:
在这里插入图片描述

缓存穿透危害

缓存穿透问题可能会使后端存储负载加大,由于很多后端持久层不具备高并发性,甚至可能造成后端存储宕机。通常可以在程序中统计总调用数、缓存层命中数、如果同一个Key的缓存命中率很低,可能就是出现了缓存穿透问题。
造成缓存穿透的基本原因有两个。第一,自身业务代码或者数据出现问题(例如:set 和 get 的key不一致),第二,一些恶意攻击、爬虫等造成大量空命中(爬取线上商城商品数据,超大循环递增商品的ID)

解决方案

解决方案一:缓存空串

从缓存中取不到的数据,在数据库中也没有取到,这时可以将key-value对写为key-null,缓存的有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。

优点 :编码简单,容易理解。
缺点 :缓存中会冗余大量无效的key-null 值,占用大量的内存空间,有可能会导致缓存容量不够,从而引发其他有效的key 被清理。

下面我们来模拟实现

导入需要的jar包
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency> 
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
对象定义

我们先定义2个对象,一个是需要缓存的空对象NullResult,一个是我们需要缓存的用户信息对象User,代码分别如下所示:

/**
 * 空对象定义
 * @author yangyanping
 * @date 2020-09-12
 */
public class NullResult implements Serializable {
}
/**
 * 用户信息对象
 * @author yangyanping
 * @date 2020-09-12
 */
public class User implements Serializable {
    private Integer id;
    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
redisTemplate 配置
/**
 * 缓存模版
 *
 * @author yangyanping
 * @date 2022-11-11
 */
@Slf4j
@Configuration
public class CacheConfig {
 /**
     * Jedis数据源的连接工厂
     */
    @Bean("readWebJedisFactory")
    public JedisConnectionFactory readWebJedisFactory(@Value("${spring.redis.host}") String host,
                                                      @Value("${spring.redis.port}") int port,
                                                      @Value("${spring.redis.password}") String password) {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setPassword(password);

        return new JedisConnectionFactory(redisStandaloneConfiguration);
    }
    
    /**
     * 缓存模版
     */
    @Primary
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(@Qualifier("readWebJedisFactory") RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // key 序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value 序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 类型 key序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // hash 类型 value序列化方式
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 让设置生效
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
单元测试代码

单元测试代码,我们分别测试用户Id = 1 (数据库不存在数据) 和 用户Id=2 (数据库中存在数据) 的情况。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class EmptyDataTest {
    private static final NullResult NULL = new NullResult();

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    public void testNullData() {
        this.findUserByCache(1);
        this.findUserByCache(2);
    }

    /**
     * 查询数据
     */
    private void findUserByCache(Integer userId){
        Object value = redisTemplate.opsForValue().get(userId);

        if (value == null) {
            User user = getUserName(userId);
            value = user == null ? NULL : user;
            redisTemplate.opsForValue().set(userId, value);

            return;
        }

        if (value instanceof NullResult) {
            System.out.println("不存在的数据:" + value);
        } else {
            System.out.println("数据库中用户信息:" + value);
        }
    }


    /**
     * 模拟数据库查询,耗时2秒
     *
     * @param userId
     * @return
     */
    private User getUserName(Integer userId) {
        try {
            Thread.sleep(2000);

            if (userId % 2 == 0) {
                return new User(userId, "yangyanping_" + userId);
            }
        } catch (Exception ex) {

        }

        return null;
    }
}

运行单元测试,输出如下:

不存在的数据:com.yyp.rdis.test.NullResult@19489b27
数据库中用户信息:User{id=2, name='yangyanping_2'}

解决方案二:使用布隆过滤器

在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

优点:有效解决缓存内存占用问题,可以节约大量内存空间。
缺点:编码复杂,而且需要定时任务重构缓存中的数据(处理已删除的数据)。理解上有一定的难度。
在这里插入图片描述

好了,我们先使用guava工具类 里的布隆过滤器BloomFilter 类,测试一下。

1 : 先放入一百万条数据到布隆过滤器中。
2 : 测试我们放入的一百万条数据,在 布隆过滤器 中是否存在。
3 : 在测试1000条,我们没有放入到布隆过滤器中的数据,测试布隆过滤器否包含。

导入jar

 <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
 </dependency>

编写测试代码:

/**
 * 布隆过滤器 测试类
 * @author yangyanping
 * @date 2020-09-11
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class BloomFilterTest {
    /**
     * 定义用户数据 total = 一百万
     */
    private static int total = 1000000;

    /**
     * 初始化布隆过滤器
     */
    private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), total, 0.01);

    /**
     * 初始化一百万的用户数据到布隆过滤器
     */
    @Before
    public void init(){
        for (int userId = 1; userId <= total; userId++) {
            // 放入数据
            bloomFilter.put(Integer.valueOf(userId));
        }
    }

    /**
     * 测试缓存穿透
     */
    @Test
    public void testBloomFilter() {
        // 测试放入的数据
        for (int userId = 1; userId <= total; userId++) {
            // 如果不 包含数据则打印
            if (!bloomFilter.mightContain(userId)) {
                System.out.println("有数据逃脱了~~~=" + userId);
            }
        }

        // 测试误差数据
        int count = 0;
        for (int userId = total + 1; userId <= total + 1000; userId++) {
            if (bloomFilter.mightContain(userId)) {
                count++;
            }
        }

        System.out.println("误伤的数量:" + count);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值