缓存穿透:
- 指用户请求的资源在缓存(redis等缓存中间件)中未命中,按照正常的逻辑,未命中的数据会去持久层(mysql/pg等)去查询并获取返回。
- 但是一半持久层能承载的访问量非常有限(如果没有按照索引查找或需要很多字段时)。 当一个缓存未命中,用户体量足够大的情况下,一瞬间会有大量的请求直接打到持久层,导致超时甚至崩溃。
- 这就需要一个可行的方案来处理类似的突发情况,当然,超时控制也是必要的。如果出现超时需要及时返回,否则超时也会造成整条业务线阻塞崩溃。
- 超时需要考虑三大层级的超时,大家熟知的Nginx 超时(网关超时)/ 进程内超时 (goroutine超时控制)/ 服务间超时控制 (分布式架构下由于网络抖动等原因,需要做超时处理,不然请求堆积导致内存爆炸)
- 这里就不讨论 这些超时控制以及如何实现,这里主要谈论 缓存击穿的方案(Go语言方案,其他语言思路也是相似的)
示例代码如下:
package main
import (
"fmt"
"sync"
"golang.org/x/sync/singleflight"
)
var sg singleflight.Group
var wg sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go Req() // 模拟同时有10个请求该资源的请求未命中缓存
}
wg.Wait()
}
// Req 模拟新进入的请求,并且缓存未命中才会执行的操作
func Req() (interface{}, error) {
defer wg.Done()
val, err, _ := sg.Do("Once", Jobs)
fmt.Println("得到返回值:", val)
return val, err
}
// 实现符合Do()第二参数的函数签名,实际上应该是对持久层的访问,此处可添加超时控制
func Jobs() (res interface{}, err error) {
fmt.Println("模拟一些操作") // 这次模拟可能会执行一次以上的Jobs(),因为第一个执行的单飞已经返回结果并释放了key,此时还有goroutine 未执行,一执行发现key还处于释放状态。
// 实际场景中,当结果返回,就会填充缓存,所以基本不会存在新的请求重新打到持久层的情况,次数在可接受范围内。
return "casso", nil
}
最后,如果硬件条件比较宽裕的情况下,还宽裕做得更为保险。
- 在kafka集群中使用特定的topic 来专门接收缓存填充的消息,在起专门的消费kafka消息的服务,在该服务内进行缓存填充
- 这样基本上不会对持久层造成什么压力,但是也要注意可能有多条重复的kafka消息,在填充缓存的服务中需要进行一定的判断,即缓存是否已存在
- 还是有很多地方可以优化的,想提高可用性,那肯定是需要牺牲一定复杂度的
- singleflight
有疑问加站长微信联系(非本文作者)