zoukankan      html  css  js  c++  java
  • redis 缓存穿透 缓存击穿 缓存雪崩

     

     

    Redis(Remote Dictionary Server ), 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting), LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

    redis作为缓存时redis将所有数据放在内存中,因为多线程会有CPU上下文的切换(导致耗时),对于内存系统来说,如果没有上下文切换那么效率就是最高的。多次读写都是在同一个CPU上的,在内存情况下效果最佳。而且redis在处理命令时是单线程。

     

    redis使用缓存目的:

    1、提高性能

    2、保护存储层

     

    一、缓存穿透

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

     
    解决方案:
    (1)缓存空对象

    在存储层没有命中的情况下,将(key,null)加入缓存层。

    缓存空对象会有两个问题:

    <1> value为null 同样会占用内存空间,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

    <2> 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。

    (2)布隆过滤器

    在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。

    算法描述:

    • 初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。
    • 添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。
    • 判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。

    构建bitmap:

     查询y1:

     查询x:

     查询z:

    错报原因:

    一个key映射数组上多位,一位会被多个key使用,也就是多对多的关系。如果一个key映射的所有位值为1,就判断为存在。但是可能会出现key1 和 key2 同时映射到下标为100的位,key1不存在,key2存在,这种情况下会发生错误率。

    使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

    public class RedisBloomFilter {
        private static final Logger LOGGER = Logger.getLogger(RedisBloomFilter.class);
        private static final String BF_KEY_PREFIX = "bf:";
    
    
        private int numApproxElements;
        private double fpp;
        private int numHashFunctions;
        private int bitmapLength;
    
    
        private JedisResourcePool jedisResourcePool;
    
    
        /**
         * 构造布隆过滤器。注意:在同一业务场景下,三个参数务必相同
         *
         * @param numApproxElements 预估元素数量
         * @param fpp               可接受的最大误差(假阳性率)
         * @param jedisResourcePool Codis专用的Jedis连接池
         */
        public RedisBloomFilter(int numApproxElements, double fpp, JedisResourcePool jedisResourcePool) {
            this.numApproxElements = numApproxElements;
            this.fpp = fpp;
            this.jedisResourcePool = jedisResourcePool;
    
    
            bitmapLength = (int) (-numApproxElements * Math.log(fpp) / (Math.log(2) * Math.log(2)));
            numHashFunctions = Math.max(1, (int) Math.round((double) bitmapLength / numApproxElements * Math.log(2)));
        }
    
    
        /**
         * 取得自动计算的最优哈希函数个数
         */
        public int getNumHashFunctions() {
            return numHashFunctions;
        }
    
    
        /**
         * 取得自动计算的最优Bitmap长度
         */
        public int getBitmapLength() {
            return bitmapLength;
        }
    }
    
     /**
         * 计算一个元素值哈希后映射到Bitmap的哪些bit上
         *
         * @param element 元素值
         * @return bit下标的数组
         */
        private long[] getBitIndices(String element) {
            long[] indices = new long[numHashFunctions];
    
    
            byte[] bytes = Hashing.murmur3_128()
                .hashObject(element, Funnels.stringFunnel(Charset.forName("UTF-8")))
                .asBytes();
    
    
            long hash1 = Longs.fromBytes(
                bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]
            );
            long hash2 = Longs.fromBytes(
                bytes[15], bytes[14], bytes[13], bytes[12], bytes[11], bytes[10], bytes[9], bytes[8]
            );
    
    
            long combinedHash = hash1;
            for (int i = 0; i < numHashFunctions; i++) {
                indices[i] = (combinedHash & Long.MAX_VALUE) % bitmapLength;
                combinedHash += hash2;
            }
    
    
            return indices;
     }
    
    /**
         * 插入元素
         *
         * @param key       原始Redis键,会自动加上'bf:'前缀
         * @param element   元素值,字符串类型
         * @param expireSec 过期时间(秒)
         */
        public void insert(String key, String element, int expireSec) {
            if (key == null || element == null) {
                throw new RuntimeException("键值均不能为空");
            }
            String actualKey = BF_KEY_PREFIX.concat(key);
    
    
            try (Jedis jedis = jedisResourcePool.getResource()) {
                try (Pipeline pipeline = jedis.pipelined()) {
                    for (long index : getBitIndices(element)) {
                        pipeline.setbit(actualKey, index, true);
                    }
                    pipeline.syncAndReturnAll();
                } catch (IOException ex) {
                    LOGGER.error("pipeline.close()发生IOException", ex);
                }
                jedis.expire(actualKey, expireSec);
            }
        }
    
    
        /**
         * 检查元素在集合中是否(可能)存在
         *
         * @param key     原始Redis键,会自动加上'bf:'前缀
         * @param element 元素值,字符串类型
         */
        public boolean mayExist(String key, String element) {
            if (key == null || element == null) {
                throw new RuntimeException("键值均不能为空");
            }
            String actualKey = BF_KEY_PREFIX.concat(key);
            boolean result = false;
    
    
            try (Jedis jedis = jedisResourcePool.getResource()) {
                try (Pipeline pipeline = jedis.pipelined()) {
                    for (long index : getBitIndices(element)) {
                        pipeline.getbit(actualKey, index);
                    }
                    result = !pipeline.syncAndReturnAll().contains(false);
                } catch (IOException ex) {
                    LOGGER.error("pipeline.close()发生IOException", ex);
                }
            }
    
    
            return result;
      }
    
    public class RedisBloomFilterTest {
        private static final int NUM_APPROX_ELEMENTS = 3000;
        private static final double FPP = 0.03;
        private static final int DAY_SEC = 60 * 60 * 24;
        private static JedisResourcePool jedisResourcePool;
        private static RedisBloomFilter redisBloomFilter;
    
    
        @BeforeClass
        public static void beforeClass() throws Exception {
            jedisResourcePool = RoundRobinJedisPool.create()
                .curatorClient("10.10.99.130:2181,10.10.99.132:2181,10.10.99.133:2181,10.10.99.124:2181,10.10.99.125:2181,", 10000)
                .zkProxyDir("/jodis/bd-redis")
                .build();
            redisBloomFilter = new RedisBloomFilter(NUM_APPROX_ELEMENTS, FPP, jedisResourcePool);
            System.out.println("numHashFunctions: " + redisBloomFilter.getNumHashFunctions());
            System.out.println("bitmapLength: " + redisBloomFilter.getBitmapLength());
        }
    
    
        @AfterClass
        public static void afterClass() throws Exception {
            jedisResourcePool.close();
        }
    
    
        @Test
        public void testInsert() throws Exception {
            redisBloomFilter.insert("topic_read:8839540:20190609", "76930242", DAY_SEC);
            redisBloomFilter.insert("topic_read:8839540:20190609", "76930243", DAY_SEC);
            redisBloomFilter.insert("topic_read:8839540:20190609", "76930244", DAY_SEC);
            redisBloomFilter.insert("topic_read:8839540:20190609", "76930245", DAY_SEC);
            redisBloomFilter.insert("topic_read:8839540:20190609", "76930246", DAY_SEC);
        }
    
    
        @Test
        public void testMayExist() throws Exception {
            System.out.println(redisBloomFilter.mayExist("topic_read:8839540:20190609", "76930242"));
            System.out.println(redisBloomFilter.mayExist("topic_read:8839540:20190609", "76930244"));
            System.out.println(redisBloomFilter.mayExist("topic_read:8839540:20190609", "76930246"));
            System.out.println(redisBloomFilter.mayExist("topic_read:8839540:20190609", "76930248"));
       }
    }
    

    优点:效果好

    缺点:难维护、删除数据难、添加数据时需要添加到布隆过滤器

     

    二、缓存击穿

    当前key是一个热点key(例如一个秒杀活动),并发量非常大。

    重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。

    在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

     

    原因:缓存中没有,数据库中有,是一个并发问题

    解决方案:
    (1)永不过期
    • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期
    • 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去更新缓存

    (2)分布式锁

    只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。set(key,value,timeout)

    127.0.0.1:6379> setnx lock value1 #在键lock不存在的情况下,将键key的值设置为value1 
    (integer) 1 
    127.0.0.1:6379> setnx lock value2 #试图覆盖lock的值,返回0表示失败 
    (integer) 0 
    127.0.0.1:6379> get lock #获取lock的值,验证没有被覆盖
    "value1" 
    127.0.0.1:6379> del lock #删除lock的值,删除成功 
    (integer) 1
    127.0.0.1:6379> setnx lock value2 #再使用setnx命令设置,返回0表示成功 
    (integer) 1 
    127.0.0.1:6379> get lock #获取lock的值,验证设置成功 
    "value2"

    两种方案对比:

    • 分布式互斥锁:这种方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低。
    • “永远不过期”:就是构建缓存其余线程(非构建缓存的线程)可能访问的是老数据
    三、缓存雪崩

    由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。

    
    

    解决方案:

    (1)高可用集群 redis cluster或者sentinel

     

    (2)采用多级缓存,本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底

    (3)缓存的过期时间用随机值,尽量让不同的key的过期时间不同

    https://www.zhihu.com/question/300767410/answer/1749442787

    https://www.cnblogs.com/williamjie/p/11132211.html

    https://www.jianshu.com/p/47fd7f86c848

    https://www.jianshu.com/p/c2defe549b40

    https://blog.csdn.net/womenyiqilalala/article/details/105205532

     

  • 相关阅读:
    滚动到Table的某个位置
    前端导出可修改样式的Excel表格
    在vue中绑定weui的class
    html5的消息通知
    在nginx上部署页面,使用ip访问页面,实现跨设备访问本地静态页面
    用原生javascript实现get请求,及具体数据的获取
    vue中如何实现图片不存在时显示一个默认的图片
    css选择器
    javascript 类型及类型判断
    CSS
  • 原文地址:https://www.cnblogs.com/xiaohaigegede/p/15696103.html
Copyright © 2011-2022 走看看