Dawn's Blogs

分享技术 记录成长

0%

从零实现分布式缓存 (6) 防止缓存击穿

本节实现实现了防止缓存击穿的措施,通过多个并发请求映射为一个请求来实现。最终代码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
lru/
|--lru.go
|--lru_test.go
singleflight/
|--singleflight.go
byteview.go
cache.go
consistenthash.go
dawncache.go
dawncache_test.go
go.mod
http.go
peers.go

概念

  • 缓存雪崩:缓存在同一时刻全部失效,造成瞬时 DB 请求量过大、压力骤增。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。
  • 缓存击穿:一个存在的 key,在缓存过期的一瞬间,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。
  • 缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB。

singleflight 实现

singleflight/singleflight.go

dawncache 通过 singleflight 来实现防止缓存击穿。

定义结构体

首先定义 call,代表一次请求:

1
2
3
4
5
6
// call 代表一次查询请求
type call struct {
wg sync.WaitGroup
val interface{}
err error
}

Group 记录了待查询的 key 和一次请求之间的映射关系。

当 key 还在 hashMap 中时,视为一次请求即可。

1
2
3
4
type Group struct {
mu sync.Mutex // 对 hashMap 的访问互斥
hashMap map[string]*call // 保存 key 和请求的映射关系
}

实现 Do 方法

Do 方法实现了多次相同的查询,到一次请求的映射操作:

  • 查询 hashMap 中是否已经记录了 key 对应的 call 操作,如果有,则等待这一次请求得到数据并返回结果。
  • 如果不在 hashMap 中,则新建一个查询请求 call 并记录在 hashMap 中,待执行过查询操作之后再返回数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if g.hashMap == nil { // 延迟初始化
g.hashMap = make(map[string]*call)
}
if c, ok := g.hashMap[key]; ok {
// 已在 hashMap 中记录,等待结果即可
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err
}

// 没有在 hashMap 中记录
// 新建 call 在 hashMap 中记录
c := new(call)
c.wg.Add(1)
g.hashMap[key] = c
g.mu.Unlock()

// 远程请求数据
c.val, c.err = fn()
c.wg.Done() // 得到数据

g.mu.Lock()
delete(g.hashMap, key)
g.mu.Unlock()

return c.val, c.err
}

修改主流程

dawncache.go

需要修改 Group 结构体,使之能够防止缓存穿透:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Group struct {
// ...
loader *singleflight.Group
}

// NewGroup 新建一个 *Group 缓存
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
// ...
g := &Group{
// ...
loader: new(singleflight.Group),
}
// ...
}

修改 load 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// load 从别处加载数据
func (g *Group) load(key string) (ByteView, error) {
view, err := g.loader.Do(key, func() (interface{}, error) {
if g.peers != nil {
// peers 不为空,可以从远程获取数据
if peer, ok := g.peers.PickPeer(key); ok {
// 从远程获取数据
view, err := g.getFromPeer(peer, key)
if err != nil {
log.Println("[GeeCache] Failed to get from peer", err)
return ByteView{}, err
}
return view, nil
}
}
// 本地通过回调函数获取数据
return g.getLocally(key)
})
if err != nil {
return ByteView{}, err
}
return view.(ByteView), nil
}