Code前端首页关于Code前端联系我们

Redis缓存渗透分析、雪崩和并发问题分析

terry 2年前 (2023-09-26) 阅读数 45 #数据库

使用redis作为缓存是很常见的,但是在使用redis之后也可能会遇到很多问题,尤其是数据量较大的时候。几个经典问题如下:

(1) 缓存与数据库之间的数据一致性问题

在分布式环境中(更不用说单机),缓存与数据库之间很容易出现数据一致性问题。关于这一点,我们只能说,如果你的项目需要强缓存一致性,就不要使用缓存。我们只能采取适当的策略来降低缓存和数据库之间数据不一致的概率,但不能保证它们之间的强一致性。适当的策略包括适当的高速缓存更新策略。数据库更新后,要及时更新缓存,并添加缓存失效时的重试机制,例如MQ模式的消息队列。

(2)缓存碎片问题

缓存碎片是指恶意用户模拟请求大量缓存中不存在的数据。由于缓存中没有数据,这些请求会在短时间内直接落到数据库,导致数据库出现异常。我们在实际项目中也遇到过这种情况。部分秒杀、秒杀活动的API被大量恶意用户下载,导致数据库短时间宕机。好在数据库有多个master和多个slave,所以可以hold住。

解决方案:

1。使用互斥锁队列

常见的行业实践是在键为空时获取一个值,锁定它,从数据库中检索数据,然后释放锁。如果其他线程获取锁失败,请稍等片刻再尝试。这里需要注意的是,在分布式环境下必须使用分布式锁。对于单机来说,普通的锁(synchronized、Lock)就足够了。

public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
 // 通过key获取value
 String value = redisService.get(key);
 if (StringUtil.isEmpty(value)) {
 // 分布式锁,详细可以参考https://blog.csdn.net/fanrenxiang/article/details/79803037
 //封装的tryDistributedLock包括setnx和expire两个功能,在低版本的redis中不支持
 try {
 boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
 if (locked) {
 value = userService.getById(key);
 redisService.set(key, value);
 redisService.del(lockKey);
 return value;
 } else {
 // 其它线程进来了没获取到锁便等待50ms后重试
 Thread.sleep(50);
 getWithLock(key, jedis, lockKey, uniqueId, expireTime);
 }
 } catch (Exception e) {
 log.error("getWithLock exception=" + e);
 return value;
 } finally {
 redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
 }
 }
 return value;
}
复制代码

这种方式思路比较清晰,一定程度上减轻了数据库的压力。然而,锁定机制增加了逻辑复杂性并降低了吞吐量,这更多的是治标不治本。

2。布隆过滤器(推荐)

bloomfilter类似于hashset,用于快速判断集合中是否存在某个元素。其典型应用场景是快速判断某个key是否存在于特定集合中。如果容器不存在则直接返回。布隆过滤器的关键在于哈希算法和容器大小。我们先简单实现一下,看看效果。我这里使用guava来实现布隆过滤器:

<dependencies> 
 <dependency> 
 <groupId>com.google.guava</groupId> 
 <artifactId>guava</artifactId> 
 <version>23.0</version> 
 </dependency> 
</dependencies> 
public class BloomFilterTest {
 
 private static final int capacity = 1000000;
 private static final int key = 999998;
 
 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
 
 static {
 for (int i = 0; i < capacity; i++) {
 bloomFilter.put(i);
 }
 }
 
 public static void main(String[] args) {
 /*返回计算机最精确的时间,单位微妙*/
 long start = System.nanoTime();
 
 if (bloomFilter.mightContain(key)) {
 System.out.println("成功过滤到" + key);
 }
 long end = System.nanoTime();
 System.out.println("布隆过滤器消耗时间:" + (end - start));
 int sum = 0;
 for (int i = capacity + 20000; i < capacity + 30000; i++) {
 if (bloomFilter.mightContain(i)) {
 sum = sum + 1;
 }
 }
 System.out.println("错判率为:" + sum);
 }
}
成功过滤到999998
布隆过滤器消耗时间:215518
错判率为:318
复制代码

可以看到,只消耗了100万条数据。密钥配对时间约为 0.2 毫秒,足够快了。然后我们模拟了1w个布隆过滤器中不存在的key,对应的错误率为318/10000,即错误率在3%左右。跟踪 BloomFilter 源码,我们发现默认的容错率为 0.03:

public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
 return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}
复制代码

我们可以调用这个 BloomFilter 方法来明确指定误报率: Redis缓存穿透、雪崩、并发问题分析

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
复制代码

根据我们的断点监控,误报率与误报率的区别率,当为0.02,默认为0.03时: Redis缓存穿透、雪崩、并发问题分析Redis缓存穿透、雪崩、并发问题分析

比较两个错误率,可以发现误报率为7298440。误报率减少了0.01,BloomFilter维护的数组大小也减少了843923。可见,默认的BloomFilter误判率为0.03是设计者考虑系统性能后得到的。价值。需要注意的是,布隆过滤器不支持删除操作。这里用来解决缓存泄漏问题的问题是:

public String getByKey(String key) {
 // 通过key获取value
 String value = redisService.get(key);
 if (StringUtil.isEmpty(value)) {
 if (bloomFilter.mightContain(key)) {
 value = userService.getById(key);
 redisService.set(key, value);
 return value;
 } else {
 return null;
 }
 }
 return value;
}
复制代码

(3)缓存雪崩问题

大量缓存key同时过期(无效),然后一大波请求立即落下。数据库,导致连接异常。

解决方案:

1。锁和队列作为缓存渗透的解决方案,实现方式同上;

2。创建备份缓存,缓存A和缓存B,A设置了超时,B没有设置超时时间,先从A读取缓存,A不读取B并更新缓存A和缓存B;

public String getByKey(String keyA,String keyB) {
 String value = redisService.get(keyA);
 if (StringUtil.isEmpty(value)) {
 value = redisService.get(keyB);
 String newValue = getFromDbById();
 redisService.set(keyA,newValue,31, TimeUnit.DAYS);
 redisService.set(keyB,newValue);
 }
 return value;
}
复制代码

(四)缓存并发问题

这里的并发是指多个redis客户端同时设置key引起的并发。更有效的解决方案是将 redis.set 操作排队以将其序列化。必须一次完成一项。具体代码就不给出了。当然,锁定也是可以的。至于为什么不在redis中使用事务,留给读者自己思考和研究。

作者:慕容千羽
链接:https://juejin.im/post/5b961172f265da0ab7198f4d
来源:掘金。商业转载请联系作者获得许可。非商业转载请注明来源。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门