singleflight是如何避免缓存击穿的?

 

前言

singleflight 做为一个香喷喷的小工具,平时开发过程中,在不同场景结合不同的回源策略来做并发控制。传言singleflight仅仅用了百行代码就解决了缓存击穿的问题,那么它是怎么做到的呢?今天一起来学习一下。

缓存击穿

什么是缓存击穿

平常在高并发系统中,会出现大量的请求同时查询同一个key的情况,假如此时这个热key刚好失效了,就会导致大量的请求都打到数据库上面去,这种现象就是缓存击穿。缓存击穿和缓存雪崩有点像,又有点不一样,缓存雪崩是指大面积的缓存失效,打崩了DB,而缓存击穿则是指一个热点key,在不停的扛着高并发,高并发集中对着这一个点进行访问,如果这个key在失效的瞬间,持续的并发到来就会穿破缓存,直接请求到数据库,就像一个完好无损的桶上凿开了一个洞,造成某一时刻数据库请求量过大,压力剧增!

如何解决

方法1
我们简单粗暴点,直接让热点数据永远不过期,定时任务定期去刷新数据就可以了。不过这样设置需要区分场景,比如电商首页可以这么做。
方法2
为了避免出现缓存击穿的情况,我们可以在第一个请求去查询数据库的时候对他加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,后面的线程进来发现已经有缓存了,就直接走缓存,从而保护数据库。但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降。需要结合实际的业务去考虑是否要这么做。
方法3
singleflight的设计思路,也会使用互斥锁,但是相对于方法二的加锁粒度会更细,这里先简单总结一下singleflight的设计原理,后面看源码在具体分析。
singleflightd的设计思路就是将一组相同的请求合并成一个请求,使用map存储,只会有一个请求到达mysql,使用sync.waitgroup包进行同步,对所有的请求返回相同的结果。

singleflight源码赏析

数据结构

singleflight的结构体定义如下
Group结构还是比较简单的,只有两个字段,m是一个map,key是相同请求的标识,value是用来保存调用信息,这个map是懒加载,其实就是在使用时才会初始化;mu是互斥锁,用来保证m的并发安全。
m存储调用信息(call)也是单独封装了一个结构:

Do方法

注释写的比较清楚,这里唯一有疑问的是区分panic和runtime错误部分吧,这个与下面的docall方法有关联,看完docall你就知道为什么了。

docall

这里来简单描述一下为什么区分panic和runtime错误,不区分的情况下如果调用出现了恐慌,但是锁没有被释放,会导致使用相同key的所有后续调用都出现了死锁,具体可以查看这个issue:https://github.com/golang/go/issues/33519。

DoChan和Forget方法

DoChan 方法相比Do方法,大致执行流程一致,只不过DoChan返回给我们的是一个通道,让我们可以进行异步等待。

注意事项

因为我们在使用singleflight时需要自己写执行函数,所以如果我们写的执行函数一直循环住了,就会导致我们的整个程序处于循环的状态,积累越来越多的请求,所以在使用时,还是要注意一点的,比如这个例子
不过这个问题一般也不会发生,我们在日常开发中都会使用context控制超时。

思考

singleflight请求是由一个协程发起,其他协程等待。而在高并发环境,等待协程特别多的情况下,仅让一个协程去重建缓存会造成系统的不稳定性,如何优化?

posted on 2021-09-30 18:12  薛大明白  阅读(280)  评论(0编辑  收藏  举报

导航