目录
缓存穿透
场景说明
恶意使用一些非法的key,key对应的数据在数据库不存在,由于不存在,故没有缓存起来,所以这些非法key的请求都到了数据库,可能会压垮数据库。
解决方法
- 一种简单粗暴的方法就是,如果从数据库返回的结果为空,仍然缓存这个空结果,但它的过期时间要设置的短一些,比如三分钟,以防Redis中缓存的数据过多,占用太多的内存。
- 采用布隆过滤器,将所有的合法结果散列到足够大的bitmap中,一个一定不存在的数据会被bitmap拦截掉,从而避免了对底层存储系统的查询压力
缓存击穿
场景说明
key对应的数据存在,但在Redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期,同时从后端DB加载数据并回设到缓存,这个时候大并发请求可能会瞬间把后端DB压垮。
解决办法
使用互斥锁
如果是单机用ReentrantLock,如果是集群则用分布式锁。并且要用到分段锁思想和double checked locking.伪代码如下:
public static String call() throws InterruptedException {
String key = "";
String value = getCache(key);
if (value != null) {
return value;
}
ReentrantLock lock = map.computeIfAbsent(key, k -> new ReentrantLock());
if (lock.tryLock(3L, TimeUnit.SECONDS)) {
try {
value = getCache(key);
if (value != null) {
return value;
}
value = getFromDB(key);
setCache(key, value);
return value;
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
} else {
value = getCache(key);
if (value != null) {
return value;
}
}
return value;
}
这种方法虽然保护了DB,但是会影响系统吞吐量。
后台定时任务刷新缓存
后台定义一个定时任务专门主动更新缓存数据,比如一个缓存中的数据过期时间是5分钟,那么定时任务每隔4分钟刷新数据。这种方案比较容易理解,但是会增加系统复杂度。比较适合那些key较少cache粒度大的业务。
检查更新
将缓存key的过期时间(绝对时间)一起保存到缓存中,可以拼接,可以添加新字段,可以采用单独的key保存。不管用什么方式,只要两者建立好关联关系就行。在每次执行get操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果 缓存过期时间-当前系统时间<=1分钟(自定义的一个值),则主动更新缓存.这样就能保证缓存中的数据始终是最新的(和方案一一样,让数据不过期.)
这种方案在特殊情况下也会有问题。假设缓存过期时间是12:00,而 11:59
到 12:00这 1 分钟时间里恰好没有 get 请求过来,又恰好请求都在 12:00 分的时候高并发过来,那就悲剧了。这种情况比较极端,但并不是没有可能。因为“高并发”也可能是阶段性在某个时间点爆发。
多级缓存
采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。
这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。代码示例如下:
public static String call(String key) throws InterruptedException {
String value = getLevel1Cache(key);
if (value != null) {
return value;
}
ReentrantLock lock = map.computeIfAbsent(key, k -> new ReentrantLock());
if (lock.getHoldCount() > 0) {
value = getLevel2Cache(key);
if (value == null) {
for (int count = 0; count < 3; count++) {
TimeUnit.MILLISECONDS.sleep(20L + count * 150);
value = getLevel1Cache(key);
if (value != null) {
return value;
}
}
}
} else {
if (lock.tryLock(500L, TimeUnit.MILLISECONDS)) {
try {
value = getLevel1Cache(key);
if (value != null) {
return value;
}
value = getFromDB(key);
setLevel1Cache(key, value);
setLevel2Cache(key, value);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
} else {
return getLevel1Cache(key);
}
}
return value;
}
缓存雪崩
场景说明
当缓存服务器重启或者大量缓存在某一个时间段失效,这样在失效的时候,也会给后端DB带来很大的压力。
解决办法
- 缓存时间设置成随机值,比如1-5分钟的随机值,这样缓存就不会同时失效
删除BigKey
问题
BigKey是指key对应的value很大,对这类bigkey直接使用del命令进行删除,会导致redis长时间阻塞,甚至崩溃,因为redis是单线程的,单个命令执行时间过长,就会阻塞其他命令。
一般来讲,字符串类型的bigkey删除都挺快的,不会引起问题,而hash、list、set、zset等类型的bigkey在删除时容易引起问题,总体上看,元素个数越多,所占的空间越大,删除越慢。
解决方案
设计上避免BigKey
不存BigKey,也就不需要解决这个问题,但有时候无法避免,比如一开始内存占用不多,慢慢才变多的。
渐进式删除
- 先将key改名,通过旧key无法访问数据了,等于逻辑上删除了key。
- 分批删除,通过 scan 命令遍历改名后的bigkey,每次取得少部分元素,对其删除,然后再获取和删除下一批元素。
unlink
Redis 4.0 推出了一个重要命令unlink,用来拯救 del 删大key的困境。unlink的工作思路:
- 在所有命名空间中把 key 删掉,立即返回,不阻塞。
- 后台线程执行真正的释放空间的操作。
UNLINK 基本可以替代 del,但个别场景还是需要 del 的,例如在空间占用积累速度特别快的时候就不适合使用UNLINK,因为 UNLINK 不是立即释放空间。
详细参考:https://blog.csdn.net/wade1010/article/details/128841411?spm=1001.2014.3001.5506
谨慎使用的命令
面试时一般会问“在生产环境谨慎使用的命令”或“使用redis时应该注意什么”,可以回答keys命令。
KEYS foo*
将返回所有以"foo"开头的键,例如"foobar"、"football"等。
如果要匹配所有以"foo"结尾的键,可以使用以下命令:
KEYS *foo
这将返回所有以"foo"结尾的键,例如"barfoo"、"bazfoo"等。
如果要匹配包含"foo"的键,可以使用以下命令:
KEYS *foo*
这将返回所有包含"foo"的键,例如"foobar"、"barfoo"等。
在Redis中,可以使用通配符 “*
” 和 “?” 来进行模糊匹配。其中,“*
” 表示匹配任意多个字符,“?” 表示匹配单个字符。
KEYS命令在处理大量键时可能会对性能产生影响,因为它需要遍历所有键来进行匹配。如果需要在生产环境中使用模糊匹配,建议使用SCAN命令来替代KEYS命令,因为SCAN命令可以逐步迭代地获取匹配的键,减少对性能的影响。
scan 0 match key99* count 1000
更详细的:https://zhuanlan.zhihu.com/p/651194482
集群数据倾斜怎么解决
参考:https://blog.csdn.net/wang0907/article/details/128373795
redis原子性保证
参考:https://www.cnblogs.com/gossip/p/13901425.html