golang防缓存击穿利器–singleflight

20次阅读

共计 1494 个字符,预计需要花费 4 分钟才能阅读完成。

缓存击穿
    给缓存加一个过期时间,下次未命中缓存时再去从数据源获取结果写入新的缓存,这个是后端开发人员再熟悉不过的基操。本人之前在做直播平台活动业务的时候,当时带着这份再熟练不过的自信,把复杂的数据库链表语句写好,各种微服务之间调用捞数据最后算好的结果,丢进了缓存然后设了一个过期时间,当时噼里啪啦两下写完代码觉得稳如铁蛋,结果在活动快结束之前,数据库很友好的挂掉了。当时回去查看监控后发现,是在活动快结束前,大量用户都在疯狂的刷活动页,导致缓存过期的瞬间有大量未命中缓存的请求直接打到数据库上所导致的,所以这个经典的问题稍不注意还是害死人
    防缓存击穿的方式有很多种,比如通过计划任务来跟新缓存使得从前端过来的所有请求都是从缓存读取等等。之前读过 groupCache 的源码,发现里面有一个很有意思的库,叫 singleFlight, 因为 groupCache 从节点上获取缓存如果未命中,则会去其他节点寻找,其他节点还没有的话再从数据源获取,所以这个步骤对于防击穿非常有必要。singleFlight 使得 groupCache 在多个并发请求对一个失效的 key 进行源数据获取时,只让其中一个得到执行,其余阻塞等待到执行的那个请求完成后,将结果传递给阻塞的其他请求达到防止击穿的效果。
SingleFlight 使用 Demo
本文模拟一个数据源是从调用 rpc 获取的场景然后再模拟一百个并发请求在缓存失效的瞬间同时调用 rpc 访问源数据效果可以看到 100 个并发请求从源数据获取时,rpcServer 端只收到了来自 client 17 的请求,而其余 99 个最后也都得到了正确的返回值。
SingleFlight 源码剖析
在看完 singleFlight 的实际效果后,欣喜若狂,想必其实现应该相当复杂吧,结果翻看源码一看, 100 行不到的代码就解决了这么个业务痛点,不得不佩服。
package singlefilght

import “sync”

type Group struct {
mu sync.Mutex
m map[string]*Call // 对于每一个需要获取的 key 有一个对应的 call
}

// call 代表需要被执行的函数
type Call struct {
wg sync.WaitGroup // 用于阻塞这个调用 call 的其他请求
val interface{} // 函数执行后的结果
err error // 函数执行后的 error
}

func (g *Group) Do(key string, fn func()(interface{}, error)) (interface{}, error) {

g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*Call)
}

// 如果获取当前 key 的函数正在被执行,则阻塞等待执行中的,等待其执行完毕后获取它的执行结果
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err
}

// 初始化一个 call,往 map 中写后就解
c := new(Call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()

// 执行获取 key 的函数,并将结果赋值给这个 Call
c.val, c.err = fn()
c.wg.Done()

// 重新上锁删除 key
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()

return c.val, c.err

}

    对的没看错,就这么 100 行不到的代码就能解决缓存击穿的问题,这算是我写过最愉快的一篇博了,同时也推荐大家去读一读 groupCache 这个项目的源码,会有更多惊喜的发现

正文完
 0