我们这次是用redis给es做缓存,但是缓存还是会面临经典的几个问题——穿透、雪崩、击穿、缓存更新策略等等。
这里采用了go的singleflight库来解决缓存击穿的问题。
1. 浅层
1.1 什么是缓存击穿
热数据失效。
个人认为,记忆这种东西,提示词(就好像那个索引),越短越好。其实面试的时候,只要想起来了缓存击穿是什么,就可以扩展出来说很多。
1.2 singleflight 能干什么呢
SingleFlight 可以将对同一条数据的并发请求进行合并,只允许一个请求访问数据库中的数据,这个请求得到的数据结果与其他请求共享。
——— 来自《亿级流量系统架构设计与实战》
具体可以看这个demo:
package main
import (
"log"
"sync"
"golang.org/x/sync/singleflight"
)
var g singleflight.Group
// 模拟数据库查询请求
func getDataFromDBV1(key string) (string, error) {
data, err, _ := g.Do(key, func() (interface{}, error) {
log.Printf("get data from db, key: %s", key)
return "hello singleflight", nil
})
if err != nil {
return "", err
}
return data.(string), nil
}
func getDataFromDBV2(key string) (string, error) {
log.Printf("get data from db, key: %s", key)
return "hello singleflight", nil
}
// 并发数=10
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
data, err := getDataFromDBV1("fake-key")
if err != nil {
return
}
log.Printf("get data success, data: %s", data)
}()
}
wg.Wait()
}
得到的结果是,只查询了数据库一次,但每个请求都获得了结果,如图。
2. 读源码
- 源码地址:go.googlesource.com/sync
- 直接clone:
git clone https://go.googlesource.com/sync
singleflight 底层是如何实现这一功能的呢?
type call struct {
wg sync.WaitGroup
val interface{}
err error
dups int
chans []chan<- Result
}
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err, true
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
defer func() {
g.mu.Lock()
defer g.mu.Unlock()
c.wg.Done()
if g.m[key] == c {
delete(g.m, key)
}
}()
c.val, c.err = fn()
}
SingleFlight 主要是利用了sync.WaitGroup, 一个请求调用WaitGroup.Add(1),然后真正执行查询函数,其他请求看到 map[key] 已经存在,则执行WaitGroup.wait() 等待结果。真正执行查询的请求获取到结果后,更新c.val, 并调用WaitGroup.Done(),让计数器减1,此时正在等待结果的其他请求就会被放行,自然就能看到更新后的结果了。
这张图来自《亿级流量系统架构理论与实战》