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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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

 

posted @   小海哥哥de  阅读(100)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示