缓存击穿及解决方案

这是我参与更文挑战的第6天,活动详情查看: 更文挑战

问题起源与某微信群大佬说自己原本支持singleflight的代码被改了~

本着程序员能有不掌握的技术,不能有不了解的名词,去搜索singleflight~进而搜索到“缓存击穿”这个名词,相关名词还有缓存雪崩

概念

先来了解一下名词的概念:

缓存击穿:指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db[1]。此时缓存起不到作用,就像被“击穿”了。缓存击穿会引起数据库瞬间压力增大。

缓存雪崩:指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

解决方案

互斥锁

当缓存失效时,给从数据库取数据添加一个锁,只能有一个线程(线程A)可以进去读取数据库,其他线程等待;当线程A读取数据库数据成功后,将数据更新到缓存中,其他等待线程直接去读取缓存获取数据。Go的singleflight用的就是这种思想。我们首先贴一段Java-redis的代码:

其中的redis.setnx方法为加锁。

setnx, SET if Not eXists 的缩写。只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。命令在设置成功时返回 1 , 设置失败时返回 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String get(String key) {  
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1") == 1) {
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}

Go的singleflight采用的也是这种互斥锁的方案,实现上singleflight使用一个临时的map存储第一个协程读取数据库的数据,后续方法直接从map中获取,连缓存都不用读。singleflight全部代码并不多,其github地址。

其源代码可在Go安装路径,如C:\Program Files\Go\src\internal\singleflight查看。

异步构建缓存

简单来说,就是获取始终从缓存获取;缓存中存储数据与过期时间,每次过期后启动另外的线程获取数据更新缓存。无法保证数据的一致性,但大多数情况下可以满足需求。

布隆过滤器

布隆过滤器的作用是能够迅速判断一个元素是否在一个集合

应用布隆过滤器到缓存击穿中,就是维护一个数据库key的集合,每次请求,首先取缓存中获取数据,缓存中没有则通过布隆过滤器判断查询的key是否可能在数据库中,若不在则不请求数据库[3]。

[1]缓存击穿-百度百科,https://baike.baidu.com/item/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF

[2]https://silenceper.com/blog/202003/singleflight/

[3]https://www.cnblogs.com/wangwust/p/9467720.html