redis 缓存穿透问题,及解决方案

        hello~大家好,我是JavaBoy_ahua,好久没有分享redis的知识点啦,记得上次还是整合redis是很简单的一个案例可以说是入了个门吧,这次我要分享的是redis 缓存穿透的一个问题,让大家更深的了解咱们的一个redis。

        ok,大家都知道咱们对于一个热点数据,都会进行一个缓存,从而提升我们应用的一个性能,减少我们数据库的一个压力。所以对于热点数据我们都会放在redis中做那么一个缓存,先去redis里面找,没找到再去咱们的数据库。

        下面是我话的一个很简单很简单的一个模型:

        有点简陋哦,大家将就一下,over 回归正题,如上图所示,假如有黑客蓄意攻击,它直接请求,咱们的数据库和redis都没有的一个key,比如根据id查询商品的一个功能,大家都知道如果id是自增,那-1这个id数据库里是绝对没有的 ,redis咱们有没有存,那请求就直接打到了咱们的数据库,直接贯穿,这就是咱们缓存穿透的一个概念。

        

           大家想一下解决方案,我猜现在肯定有小伙伴已经想到了一个解决方案,就是说人家请求-1,咱们数据库中虽然没有,但请求完之后我就给redis存一个key:-1,value:null,并设置一个较短的过期时间,那人家第二次用-1请求不就直接被咱们的redis拦截下来了嘛。

        哈哈哈这个办法确实可以,但是人家也不傻呀,我可以不用-1,我用-2,-3甚至用UUID这类的呢而且这个方法第一次还是会请求到数据库,如何在redis上读写,对redis也有一个压力。

        所以咱们怎么办呢?

大家有没有想过,咱们在redis和数据库之间再去给人家过滤一遍,咱们先把所有的id查出来放在一个数组里面 然后请求-1过来,穿过了我的redis,然后我再去我那个数组里面判断 如果该数组里面没有这个id之间就给人家返回一个null。

        唉~问题又来了,假如咱们有数据库有几十万条,几百万条数据,放在一个数组里岂不是占用咱们的内存而且还特别慢。所以咱们的解决方案它来啦!!!

  对,就是布隆过滤器,人家用的是hash的一系列算法再hash散列里面去占了位置,所以性能特别的高。

当然布隆过滤器我这就不深讲啦,感兴趣的小伙伴可以自己去了解一下。下面是加上了bloomFilter的一个场景

        

        对啦,布隆过滤器如何使用呢,我这写了两个案例,一个是redission自带的布隆,还有一个是guava的布隆过滤器。

首先是redission的

  /**
     * 模拟缓存穿透 解决方案布隆过滤器
     */
    @Test
    public void bloomTest(){
        Config config=new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        //构造Redission
        RedissonClient redission = Redisson.create(config);
        RBloomFilter<String> bloomFilter = redission.getBloomFilter("phoneList");
        //初始化布隆过滤器
        bloomFilter.tryInit(10000L,0.03);
        //将号码1008611插入到布隆过滤器中
        bloomFilter.add("1008611");
        //判断下面号码是否在布隆过滤器中
        System.out.println("123456:"+bloomFilter.contains("123456"));
        System.out.println("1008611:"+bloomFilter.contains("1008611"));
    }

紧接着是guava的

  @Test
    public void guavaBloomFilterTest(){
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 10000, 0.01);
        bloomFilter.put("1008611");
        //判断下面号码是否在布隆过滤器中
        System.out.println("123456:"+bloomFilter.mightContain("123456"));
        System.out.println("1008611:"+bloomFilter.mightContain("1008611"));
    }

最后要注意的是,bloomFilter有一个容错率,容错率越高性能越快,容错率越低性能越低窝,所以使用布隆过滤器的时候大家得预估一下自己的数据量和可容错率。

为什么会有这么一个容错率呢,因为就这么几个位置,hash来hash去总会占满的啦,然后为什么容错率越低效率越慢呢,因为你给的容错率越低它的hash运算方法就越多,所以会导致一个性能低的一个场景。

当然啦咱们还是用自己封装好的呀。

首先写两个工具类

RedisBloomFilter:


import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

/**
 * @author shenwang
 * @version 1.0
 */
@Service
public class RedisBloomFilter {
    /**
     * 日志
     */
    private static final Logger logger= LoggerFactory.getLogger(RedisBloomFilter.class);
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 根据给定的布隆过滤器添加值
     */
    public <T> void addByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            logger.info("----------key : " + key + " " + "value : " + i+"----------");
            redisTemplate.opsForValue().setBit(key, i, true);
        }
    }

    /**
     * 根据给定的布隆过滤器判断值是否存在
     */
    public <T> boolean includeByBloomFilter(BloomFilterHelper<T> bloomFilterHelper, String key, T value) {
        Preconditions.checkArgument(bloomFilterHelper != null, "bloomFilterHelper不能为空");
        int[] offset = bloomFilterHelper.murmurHashOffset(value);
        for (int i : offset) {
            logger.info("----------key : " + key + " " + "value : " + i+"----------");
            if (!redisTemplate.opsForValue().getBit(key, i)) {
                return false;
            }
        }
        return true;
    }
}

BloomFilterHelper:


import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;

import java.io.Serializable;

/**
 * @author shenwang
 * @param <T>
 */
public class BloomFilterHelper<T> implements Serializable {

    private int numHashFunctions;

    private int bitSize;

    private Funnel<T> funnel;

    public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
        Preconditions.checkArgument(funnel != null, "funnel不能为空");
        this.funnel = funnel;
        // 计算bit数组长度
        bitSize = optimalNumOfBits(expectedInsertions, fpp);
        // 计算hash方法执行次数
        numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
    }

    public int[] murmurHashOffset(T value) {
        int[] offset = new int[numHashFunctions];

        long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
        int hash1 = (int) hash64;
        int hash2 = (int) (hash64 >>> 32);
        for (int i = 1; i <= numHashFunctions; i++) {
            int nextHash = hash1 + i * hash2;
            if (nextHash < 0) {
                nextHash = ~nextHash;
            }
            offset[i - 1] = nextHash % bitSize;
        }

        return offset;
    }

    /**
     * 计算bit数组长度
     */
    private int optimalNumOfBits(long n, double p) {
        if (p == 0) {
            // 设定最小期望长度
            p = Double.MIN_VALUE;
        }
        int sizeOfBitArray = (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
        return sizeOfBitArray;
    }

    /**
     * 计算hash方法执行次数
     */
    private int optimalNumOfHashFunctions(long n, long m) {
        int countOfHash = Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        return countOfHash;
    }
}

调用咱们的工具类去解决缓存穿透

import com.mjs.common.pojo.Role;
import com.mjs.common.utils.BloomFilterHelper;
import com.mjs.common.utils.RedisBloomFilter;
import com.mjs.dao.RoleMapper;
import com.mjs.service.RoleService;
import jodd.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author shenwang
 * @version 1.0
 * @date 2021/8/24 17:56
 */
@Service("roleService")
public class RoleServiceImpl implements RoleService {
    private static final Logger logger= LoggerFactory.getLogger(RoleServiceImpl.class);
    @Resource
    private RoleMapper roleMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedisBloomFilter redisBloomFilter;
    @Autowired
    private BloomFilterHelper bloomFilterHelper;

    /**
     * 根据id获取角色信息
     * @return
     */
    @Override
    public Role selectById(Integer id) {
        Role role = (Role) redisTemplate.boundValueOps(id.toString()).get();
        if (role==null){
            Long bloom = redisTemplate.opsForValue().size("bloom");
            if (bloom<1)
            {
                logger.info("---------添加了布隆过滤器----------------");
                //向布隆过滤器里面添加值
                List<Role> roles = roleMapper.selectList(null);
                for (Role r : roles) {
                    //将所有的资源id放入到布隆过滤器中
                    redisBloomFilter.addByBloomFilter(bloomFilterHelper,"bloom",r.getId().toString());
                }
            }
            //如果布隆过滤器中没有数据则直接返回null,不再经过数据库
            boolean flag = redisBloomFilter.includeByBloomFilter(bloomFilterHelper,"bloom",id.toString());
            if(!flag){
                return null;
            }
            logger.info("---------------------经过了数据库--------------");
            //查询数据库并赋值
            role= roleMapper.selectById(id);
            //将热点数据写入redis
            redisTemplate.boundValueOps(id.toString()).set(role);
           //addCache(id.toString(),role,30L);
        }
        return role;
    }
}

我也是昨天才了解到这个知识点哒,期间查了很多博客,看了一些视频,问了咱们老大,我就把自己所有碰到的知识点都整理了一下,如果有错误,希望大家可以帮我指正一下。

        后面的话我会去研究redis 缓存击穿的问题,后续也会更新博客,一起加油呀!!!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值