美文网首页
缓存击穿的风险和应对

缓存击穿的风险和应对

作者: 右耳菌 | 来源:发表于2022-09-04 23:29 被阅读0次

1. 缓存击穿场景重现


2. 思路

思路: 查询之前先判断目标数据是否存在,不存在的直接忽略。将流量拦截于缓存和数据库之前。

3. 分析

  • **问题的本质: **
    判断集合中是否存在某个元素。

  • 类似的场景:
    数据库设计中,海量数据查询,快速判断数据是否存在?
    数十亿网站域名,快速判定网址不合规。
    垃圾邮件快速判定;
    爬虫应用中的URL地址去重场景;

  • 不合理的做法:
    把数据全部加载到内存

4. 设计

目的: 减少内存占用
方式: 不保存所有ID信息,只在内存中做一个标记

布隆过滤器(Bloom Filter)

布隆过滤器(Bloom Filter)是1970年由布隆提出的。
它实际上是一个很长的二进制数组和一系列hash函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。
它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

5. 自研布隆过滤器

利用Redis特性和命令: bitmaps (SETBIT设置指定位置的值、GETBIT获取值)可以理解为这是Redis自带的二进制数组特性。

  • 例子
package cn.lazyfennec.cache.redis.service;

import com.study.cache.redis.annotations.NeteaseCache;
import com.study.cache.redis.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

@Service // 默认 单实例
public class UserService {

    @Autowired
    JdbcTemplate jdbcTemplate; // spring提供jdbc一个工具(mybastis类似)

    @Autowired
    RedisTemplate redisTemplate;

    // 生产环境:第一次初始化,后续,根据数据变化,自动维护
    @PostConstruct // 演示需要 --- 启动的时候去初始化布隆过滤器。
    public void init(){
        // 1. 加载所有数据 TODO 演示
        for (int i = 0; i < 1; i++) {
            String userId = "10001";
            int hashValue = Math.abs(userId.hashCode());
            long index = (long) (hashValue % Math.pow(2, 32)); // 元素  和 数组的映射
            // 设置Redis里面二进制数据中的值,对应位置 为 1
            redisTemplate.opsForValue().setBit("user_bloom_filter", index, true);
        }
    }

    /**
     * 根据ID查询用户信息 (redis缓存,用户信息以json字符串格式存在(序列化))
     */
    // @Cacheable(value="user", key = "#userId")// 返回值 存到redis: value -- key: user::10001
    public User findUserById(String userId) throws Exception {
        // 提前查询
        int hashValue = Math.abs(userId.hashCode());
        long index = (long) (hashValue % Math.pow(2, 32)); // 元素  和 数组的映射
        Boolean result = redisTemplate.opsForValue().getBit("user_bloom_filter", index);
        if(!result) {
            System.out.println("数据不存在" + userId);
            return null;
        }

        // 压根就不存在
        // 1. 先读取缓存
        Object cacheValue = redisTemplate.opsForValue().get(userId);
        if (cacheValue != null) {
            System.out.println("###从缓存读取数据");
            return (User) cacheValue;
        }
        // 2. 没有缓存读取数据 --- 连接,并发 支撑非常小 ---
        String sql = "select * from tb_user_base where uid=?";
        User user = jdbcTemplate.queryForObject(sql, new String[]{userId}, new BeanPropertyRowMapper<>(User.class));
        System.out.println("***从数据库读取数据");
        // 3. 设置缓存
        redisTemplate.opsForValue().set(userId, user);
        return user;
    }

    @CacheEvict(value = "user", key = "#user.uid") // 方法执行结束,清除缓存
    public void updateUser(User user) {
        String sql = "update tb_user_base set uname = ? where uid=?";
        jdbcTemplate.update(sql, new String[]{user.getUname(), user.getUid()});
    }

    /**
     * 根据ID查询用户名称
     */
    // 我自己实现一个类似的注解
    @NeteaseCache(value = "uname", key = "#userId") // 缓存
    public String findUserNameById(String userId) {
        // 查询数据库
        String sql = "select uname from tb_user_base where uid=?";
        String uname = jdbcTemplate.queryForObject(sql, new String[]{userId}, String.class);

        return uname;
    }
}

6. 布隆过滤器图示

  • 优点:
    • 内存空间占用少;
  • 缺点:
    • 布隆过滤器需要不断维护,带来新的工作
    • 布隆过滤器并不能精准过滤。
      (布隆过滤器判定不存在,100%不存在,判断为存在,则可能不存在的。)
      理论上Hash计算值是有碰撞的(不同的内容hash计算出同样的值),导致不存在的元素可能会被判断为存在

布隆过滤器并非拦截所有请求,意在将缓存击穿控制在一定的量!


如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~

相关文章

网友评论

      本文标题:缓存击穿的风险和应对

      本文链接:https://www.haomeiwen.com/subject/qmfvnrtx.html