6. 防止缓存击穿

Kesa...大约 3 分钟golang

https://github.com/dreamjz/golang-notes/tree/main/books/7-days-golang/GeeCache/day6-single-flightopen in new window

DAY6-SINGLE-FLIGHT
│  go.mod
│  go.work
│  main.go
│  run.sh
│  
└─geecache
    │  byteview.go
    │  cache.go
    │  consistenthash.go
    │  consistenthash_test.go
    │  geecache.go
    │  geecache_test.go
    │  go.mod
    │  http.go
    │  peers.go
    │
    ├─consistenthash
    │      consistenthash.go
    │      consistenthash_test.go
    │
    ├─lru
    │      lru.go
    │      lru_test.go
    │
    └─singleflight
            singleflight.go

1. 缓存雪崩、缓存击穿与缓存穿透

缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。

缓存击穿:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。

缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。

2. Single Flight

在之前的测试中,若同时发起大量的请求,会可能导致两种情况:

  1. 若需本地载入 value,会同时发起大量的数据库调用,导致缓存击穿
  2. 若需从其他节点载入,则会同时发起大量的 HTTP 请求,并且可能导致目标节点出现缓存击穿

实际上,发起的多次请求,因为需要的结果相同,只需要一个请求成功执行并返回结果即可。

2.1 实现

package singleflight

import "sync"

type call struct {
	wg  sync.WaitGroup
	val any
	err error
}

type Group struct {
	mu sync.Mutex
	m  map[string]*call
}
  • call:表示请求
    • wg:并发计数器,用于确保请求只会被调用一次
    • val:请求的结果
    • err:请求的 error
  • Group
    • mu:互斥锁,保护m的读写
    • m:存储不同的 key 对应的请求
func (g *Group) Do(key string, fn func() (any, error)) (any, error) {
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[string]*call) // 延迟加载
	}
	// 调用已存在
	if c, ok := g.m[key]; ok {
		g.mu.Unlock()
		// 等待调用结束
		c.wg.Wait()
		// 直接返回
		return c.val, c.err
	}
	// 调用不存在,创建新调用
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	// 发起调用
	c.val, c.err = fn()
	c.wg.Done()

	// 调用结束,删除调用
	g.mu.Lock()
	delete(g.m, key)
	g.mu.Unlock()

	return c.val, c.err
}
  1. 若调用表不存在,则创建(延迟加载)
  2. 若key对应的调用已存在,则等待调用完成并返回
  3. 若不存在,则创建新的调用
  4. 执行调用函数fn
  5. 调用结束,从调用表中删除
  6. 返回结果

Do保证了任意多次相同的 key 对应的调用函数只会被执行一次,并返回相同的结果。

2.2 使用

type Group struct {
	...
	// use singleflight.Group to make sure that
	// each key is only fetched once
	loader *singleflight.Group
}

func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
	...
	g := &Group{
		...
		loader:    &singleflight.Group{},
	}
	...
}


func (g *Group) load(key string) (ByteView, error) {
	view, err := g.loader.Do(key, func() (any, error) {
		if g.peers != nil {
			if peer, ok := g.peers.PickPeer(key); ok {
				val, err := g.getFromPeer(peer, key)
				if err == nil {
					return val, nil
				}
				log.Println("[GeeCache] Failed to get from peer", err)
			}
		}

		return g.getLocally(key)
	})

	if err != nil {
		return ByteView{}, err
	}

	return view.(ByteView), nil
}

load方法中,将原有的执行逻辑放入g.loader.Do中执行,保证每个请求只会被执行一次。

3. Demo 测试

发起 10 请求,可以看到请求实际只执行了 3 次。

2023/10/11 07:00:28 [Server http://localhost:8003] Pick peer http://localhost:8001
2023/10/11 07:00:28 [Server http://localhost:8001] GET /geecache/scores/Tom
2023/10/11 07:00:28 [SlowDB] search key Tom
0023/10/11 07:00:28 [Server http://localhost:8003] Pick peer http://localhost:8001
2023/10/11 07:00:28 [Server http://localhost:8001] GET /geecache/scores/Tom
2023/10/11 07:00:28 [GeeCache] hit
2023/10/11 07:00:28 [Server http://localhost:8003] Pick peer http://localhost:8001
2023/10/11 07:00:28 [Server http://localhost:8001] GET /geecache/scores/Tom
2023/10/11 07:00:28 [GeeCache] hit
...

Reference

  1. https://geektutu.com/post/geecache-day6.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2