Redis入门(二)之缓存穿透

1.写在前面

上一篇博客,我大概的介绍了下Redis的一些常用的API以及Redis的持久化的一些的内容,够大家应付工作是完全没有问题的,今天要讲的就是缓存的三大问题中的缓存穿透的问题,后面我会分成三篇的博客的样子,分别的介绍缓存的三大问题,缓存穿透,缓存击穿,缓存雪崩。在介绍缓存穿透的开始前,我会简单的介绍下Redis的一些从基本类型扩展出来的一些的类型。废话不多说,我们直接开始吧!

2.Redis的其他的类型

2.1GEO

主要是用来计算经度和纬度。

常用的API:

GEOADD locations 116.419217 39.921133 beijing

GEOPOS locations beijing

GEODIST locations tianjin beijing km 	计算距离

GEORADIUSBYMEMBER locations beijing 150 km  通过距离计算城市

注意:没有删除命令  它的本质是zset (type locations) 

所以可以使用zrem key member  删除元素

zrange key  0   -1  表示所有   返回指定集合中所有value

2.2HyperLogLog

Redis 在 2.8.9 版本添加了 HyperLogLog 结构。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

常用的API:

PFADD 2017_03_06:king 'yes' 'yes' 'yes' 'yes' 'no'

PFCOUNT 2017_03_06:king  统计有多少不同的值

PFADD 2017_09_08:king uuid9 uuid10 uu11

PFMERGE 2016_03_06:king 2017_09_08:king  合并

注意:本质还是字符串 ,有容错率,官方数据是0.81%

2.3Bitmaps

常用的API:

setbit king 500000 0

getbit king 500000 

bitcount king

Bitmap本质是string,是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset)。string(Bitmap)最大长度是512 MB,所以它们可以表示2 ^ 32=4294967296个不同的位。

这儿还是简单的介绍一个应用:朋友圈点赞功能。要实现的功能如下:

  • 点赞
  • 取消点赞
  • 统计这条朋友圈的点赞数
  • 查看是否点赞

传统的方式就是将这些数据存到数据库,比如说朋友圈的ID和点赞用户的ID存到数据库中去。但是现在我们用一个简单的方法来实现上面的功能,就是不存数据库的方式。但是局限性比较大,这儿我们只是应付一些简单的需求。

我们接下来说说每个功能如何实现?

首先是点赞的功能,我们可以用setbit 1000 100 1命令,其中1000是朋友圈的ID,然后100是点赞人的ID,然后1就是100位设置为1,这样就表示了用户ID为100的人点赞了这条ID为1000的朋友圈。

再然后是取消点赞的功能,我们可以用setbit 1000 100 0命令,其中1000是朋友圈的ID,然后100是点赞人的ID,然后0就是100位设置为0,这样就表示了用户ID为100的人取消点赞了这条ID为1000的朋友圈。

然后就是统计这条朋友圈的点赞数的功能,我们可以使用bitcount 1000,其中1000是朋友圈的ID,然后bitcount就是统计朋友圈ID为1000的值中有多少个1,这样就达到了统计这条朋友圈的点赞数了。

最后就是查看是否点赞的功能,我们可以使用gitbit 1000 100,其中1000是朋友圈的ID,然后100表示要检查用户是否点赞的用户的ID,如果返回的是1表示这个用户ID为100的人点赞了这条朋友圈,如果返回的是0表示这个用户ID为100的人没有点赞这条朋友圈。

于是我们这儿可以写出如下的Java的代码,具体的如下:先定义一个实体类,用来存储朋友圈的ID,用户的ID,状态是点赞 还是取消点赞。

package com.ys.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Talk {

    private String id;  //朋友圈id

    private Integer likeUserId;  //点赞的用户id

    private boolean status;  //状态   是点赞 还是取消点赞
}
package com.ys.service.impl;

import com.ys.entity.Talk;
import com.ys.service.LikeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;

@Service
public class LikeServiceImpl implements LikeService {

    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 点赞取消点赞
     * @param talk 封装好的点赞的对象
     * @return 是否成功
     */
    @Override
    public boolean likeAndCancelLike(Talk talk) {
        boolean execute = true;
        try {
            redisTemplate.execute(new RedisCallback<Boolean>() {
                @Nullable
                @Override
                public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    Boolean aBoolean = redisConnection.setBit(talk.getId().getBytes(), talk.getLikeUserId(), talk.isStatus());
                    redisConnection.close();
                    return aBoolean;
                }
            });
        } catch (Exception e) {
            execute = false;
        }
        return execute;
    }


    /**
     * 获取点赞数
     * @param talk 封装好的点赞的对象
     * @return 点赞的人数
     */
    @Override
    public long getLikeCount(Talk talk) {
        Object execute = redisTemplate.execute(new RedisCallback<Long>() {

            @Nullable
            @Override
            public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                Long aLong = redisConnection.bitCount(talk.getId().getBytes());
                redisConnection.close();
                return aLong;
            }
        });
        return (long) execute;
    }

    /**
     * 是否点赞
     * @param talk 封装好的点赞对象
     * @return 是否点赞
     */
    @Override
    public boolean isLike(Talk talk) {
        return (boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
            @Nullable
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                Boolean bit = connection.getBit(talk.getId().getBytes(), talk.getLikeUserId());
                connection.close();
                return bit;
            }
        });
    }
}

这段的代码就是对着我前面的介绍的命令写出来的,相信大家看着这串的代码再对着前面的描述的功能,大家应该都懂了。

拓展:Redis中字符串的底层的存的是什么?存的是二进制,那么我们又怎么证明呢?我们可以用bitmaps来证明,首先我们往Redis中存储一个键为king,值为abc的值,然后我们用Python去查看这个abc的二进制的值是多少?具体的如下:

在这里插入图片描述

在这里插入图片描述

也就是说a和b的二进制,只要将第6位变成1,第7位变成0,那么a就变成了b,我们可以用bitmaps执行下面的操作,具体的如下:

在这里插入图片描述

上面的例子可以发现字符串底层最终存的是二进制的数据。

3.缓存

3.1缓存粒度控制

通俗来讲,缓存粒度问题就是我们在使用缓存时,是将所有数据缓存还是缓存部分数据?

数据类型通用性空间占用(内存空间+网络码率)代码维护
全部数据简单
部分数据较为复杂

缓存粒度问题是一个容易被忽视的问题,如果使用不当,可能会造成很多无用空间的浪费,可能会造成网络带宽的浪费,可能会造成代码通用性较差等情况,必须学会综合数据通用性、空间占用比、代码维护性 三点评估取舍因素权衡使用。

3.2缓存穿透问题

缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,并且出于容错考虑, 如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

可能造成原因:

1.业务代码自身问题
2.恶意攻击。爬虫等等

危害

对底层数据源压力过大,有些底层数据源不具备高并发性。 例如mysql一般来说单台能够扛1000-QPS就已经很不错了。

现象

我们先来简单的演示下,对应的缓存的穿透的问题。具体的代码如下:

public R redisFindCache(String key, long expire, TimeUnit unit, CacheLoadble<T> cacheLoadble, boolean b) {

  //查询缓存
  Object redisObj = valueOperations.get(String.valueOf(key));
  
  //命中缓存
  if (redisObj != null) {
    //正常返回数据
    return new R().setCode(200).setData(redisObj).setMsg("OK");
  }
  
  T load = cacheLoadble.load();//查询数据库
  if (load != null) {
    valueOperations.set(key, load, expire, unit);  //加入缓存
    return new R().setCode(200).setData(load).setMsg("OK");
  }
  
  return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果");
}

这是一个正常的逻辑就是先查询缓存,然后缓存查到的话,就正常返回数据,如果查询不到数据就直接去查数据库,运行的结果如下:

在这里插入图片描述

这是第一个访问,缓存中是没有的,所以这儿是查数据库,这个时候我们第二次访问看看,

在这里插入图片描述

发现没有走缓存,直接走了Redis,一切似乎很完美,但是往往事在人为,这个时候有人一直访问数据中不存在的内容,你会发现,打印下面的东西

在这里插入图片描述

你会发现一直查询数据库,如果有人一直恶意破坏,这个时候你的数据就直接会崩了,那么有没有什么解决办法,

解决方案

于是我们想到了可以缓存空的对象,具体的代码如下:

public R redisFindCache(String key, long expire, TimeUnit unit, CacheLoadble<T> cacheLoadble, boolean b) {
        //查询缓存
        Object redisObj = valueOperations.get(String.valueOf(key));
        //命中缓存
        if (redisObj != null) {
            //正常返回数据
            return new R().setCode(200).setData(redisObj).setMsg("OK");
        }
        T load = cacheLoadble.load();//查询数据库
        if (load != null) {
            valueOperations.set(key, load, expire, unit);  //加入缓存
            return new R().setCode(200).setData(load).setMsg("OK");
        }else {
            valueOperations.set(key, new NullValueResultDO(), expire, unit);  //加入缓存
            return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果");
        }
        
    }

似乎我们解决了问题,但是,如果我们一直访问的都是我们没有存在缓存和数据库中的东西,那么是不是意味着我们的解决的方案又不可行。似乎是有点问题。

于是有了第二种解决办法,就是布隆过滤器,那么布隆过滤器原理是什么呢?就是将一个数进行对应次数的hash的算法,然后得到对应值,然后将这些对应的值在布隆过滤器的数组中的对应的位置改成1,下次如果要查一个数据是否存在于数据库的时候,直接对这个数进行相同次数的hash,然后找到对应的位置看看是否都为1,如果都会1,就表示有可能存在,如果有一个值为0的话,这个数一定不会存在于数据库,从而降低对数据库的访问压力。布隆过滤器主要分为以下两种:

  1. Google布隆过滤器的缺点

    基于JVM内存的一种布隆过滤器

    重启即失效

    本地内存无法用在分布式场景

    不支持大数据量存储

    如何使用如下:

    package com.ys.test;
    
    import com.google.common.hash.BloomFilter;
    import com.google.common.hash.Funnels;
    
    import java.util.ArrayList;
    import java.util.List;
    
    
    public class TestBloom {
    
        //容量
        static int insertions = 10000000;
    
        //误差
        static double fpp = 0.0001;
    
        static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), insertions, fpp);
    
        static List<Integer> list = new ArrayList<>();
    
        public static void main(String[] args) {
            for (int i = 0; i < insertions; i++) {
                bloomFilter.put(i);
            }
    
            for (int i = insertions; i < insertions + insertions; i++) {
                if (bloomFilter.mightContain(i)) {
                    list.add(i);
                }
            }
    
            System.out.println(list.size());
        }
    
    }
    

    运行结果如下:

    在这里插入图片描述

    可以看到这个误差差不多是我设置的,切记这儿不能设置成0,不然会直接报错。

    但是我们现在都是分布式的环境,所以这个Google的布隆过滤器似乎不太适用大部分的场景。于是我们打算手写一个布隆过滤器,用Redis的bitmaps来实现,具体的代码如下:

    package com.ys.filter;
    
    import com.google.common.hash.Funnels;
    import com.google.common.hash.Hashing;
    import lombok.Data;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Scope;
    import org.springframework.dao.DataAccessException;
    import org.springframework.data.redis.connection.RedisConnection;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Component;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.Pipeline;
    
    import javax.annotation.PostConstruct;
    import java.nio.charset.Charset;
    import java.nio.charset.StandardCharsets;
    import java.util.List;
    
    @ConfigurationProperties("bloom.filter")
    @Component
    public class RedisBloomFilter {
    
        //预计插入量
        private long expectedInsertions;
    
        //可接受的错误率
        private double fpp;
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        //bit数组长度
        private long numBits;
    
        //hash函数数量
        private int numHashFunctions;
    
        public long getExpectedInsertions() {
            return expectedInsertions;
        }
    
        public void setExpectedInsertions(long expectedInsertions) {
            this.expectedInsertions = expectedInsertions;
        }
    
        public void setFpp(double fpp) {
            this.fpp = fpp;
        }
    
        public double getFpp() {
            return fpp;
        }
    
        //初始化中的两个值的这儿的算法是从Google的不拢过来器哪儿拷贝过来
        @PostConstruct
        public void init() {
            this.numBits = optimalNumOfBits(expectedInsertions, fpp);
            this.numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
        }
    
        //计算hash函数个数
        private int optimalNumOfHashFunctions(long n, long m) {
            return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
        }
    
        //计算bit数组长度
        private long optimalNumOfBits(long n, double p) {
            if (p == 0) {
                p = Double.MIN_VALUE;
            }
            return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
        }
    
        /**
         * 判断key 是否存在于集合
         *
         * @param key 传入的key
         * @return 存在就返回true,不存在就返回false
         */
        public boolean isExist(String key) {
            long[] indexs = getIndexs(key);
            List list = redisTemplate.executePipelined(new RedisCallback<Object>() {
                @Nullable
                @Override
                public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    redisConnection.openPipeline();
                    for (long index : indexs) {
                        redisConnection.getBit("bf:taibai".getBytes(), index);
                    }
                    redisConnection.close();
                    return null;
                }
            });
            return !list.contains(false);
        }
        
        /**
         * 将key存入Redis bitmap
         *
         * @param key 传入的key
         */
        public void put(String key) {
            long[] indexs = getIndexs(key);
            redisTemplate.executePipelined(new RedisCallback<Object>() {
    
                @Nullable
                @Override
                public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    redisConnection.openPipeline();
                    for (long index : indexs) {
                        redisConnection.setBit("bf:taibai".getBytes(), index, true);
                    }
                    redisConnection.close();
                    return null;
                }
            });
        }
    
        /**
         * 根据key获取bitmap的下标
         *
         * @param key 传入的key
         * @return 一个hash函数对
         */
        private long[] getIndexs(String key) {
            long hash1 = hash(key);
            long hash2 = hash1 >>> 16;
            long[] result = new long[numHashFunctions];
            for (int i = 0; i < numHashFunctions; i++) {  //numHashFunctions  hash函数的数量
                long combinedHash = hash1 + i * hash2;
                if (combinedHash < 0) {
                    combinedHash = ~combinedHash;
                }
                result[i] = combinedHash % numBits;
            }
            return result;
        }
    
        /**
         * 根据key获取对应的hash值
         *
         * @param key 传入的key
         * @return 对应的hash值
         */
        private long hash(String key) {
            Charset charset = StandardCharsets.UTF_8;
            return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
        }
    }
    
    

    这就是自己手动的实现的简单的Redis的布隆过滤器,因为Redis在分布式中只有一份,所以这儿存到Redis中,所有的服务都是可以用的,同时重启了还可以用。所以在项目中是如何使用的,走来我们需要将数据库中常用的数据存入到数据库中去,具体的如下:

    package com.ys.datainit;
    
    import com.ys.entity.Order;
    import com.ys.filter.RedisBloomFilter;
    import com.ys.service.OrderService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import java.util.List;
    
    @Component
    public class RedisDataInit {
    
        @Autowired
        OrderService orderService;
    
        @Autowired
        RedisBloomFilter redisBloomFilter;
    
        @PostConstruct
        public void init() {
            List<Order> orders = orderService.selectOrderyAll();
            for (Order order : orders) {
                redisBloomFilter.put(String.valueOf(order.getId()));
            }
        }
    }
    

    然后修改刚刚缓存的代码,具体的如下:

    public R redisFindCache(String key, long expire, TimeUnit unit, CacheLoadble<T> cacheLoadble, boolean b) {
            if (!bloomFilter.isExist(key)) {
                return new R().setCode(600).setData(new NullValueResultDO()).setMsg("非法访问");
            }
            //查询缓存
            Object redisObj = valueOperations.get(String.valueOf(key));
            //命中缓存
            if (redisObj != null) {
                //正常返回数据
                return new R().setCode(200).setData(redisObj).setMsg("OK");
            }
            T load = cacheLoadble.load();//查询数据库
            if (load != null) {
                valueOperations.set(key, load, expire, unit);  //加入缓存
                return new R().setCode(200).setData(load).setMsg("OK");
            }
            return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果");
        }
    

    这个时候我们走来就来访问布隆过滤器,如果不存在,就直接返回非法访问,这个时候我们启动一下项目看看,具体的如下:

    在这里插入图片描述

    可以发现我们的项目一启动的时候,这个时候就往缓存中存入几条数据,这儿就是我数据库中的所有的记录。然后我们访问对应的网站两次,这个时候访问的数据存在数据库中,然后看控制台打印出来什么,具体的如下:

    在这里插入图片描述

    可以发现我们这儿控制台就答应出来一条的记录,那么我们访问一条数据库中不存在的数据看看,具体的如下:

    在这里插入图片描述

    直接返回非法访问,证明我没有访问数据库。从控制层面降低了无效的很多的请求。从而解决缓存穿透的问题。

4.写在最后

本篇博客大概的介绍了缓存的三大问题中的缓存穿透的问题,后面还有其他的问题,后面的博客继续讲。这篇博客就介绍到这儿了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值