Go语言使用 singleflight 防缓存击穿
1 2 3 4 5
| 1、缓存击穿 2、Go语言的 singleflight 介绍 3、singleflight 实例 4、singleflight 源码分析 5、小结
|
1、缓存击穿
缓存击穿是指在高并发的情况下,某个热点的key突然过期,导致大量的请求直接访问数据库,造成数据库的压力过大,甚至宕机的现象;
常见的解决缓存击穿方案:
① 设置热点数据永不过期:对于一些确定的热点数据,可以将其设置为永不过期,这样就可以确保不会因为缓存失效而导致请求直接访问到数据库;
② 使用互斥锁:为了防止缓存失效时所有请求同时访问到数据库,可以采用锁机制确保仅有一个请求数据库并更新缓存,而其他请求则在缓存更新后再进行访问;
③ 监控狗程序:后台监控缓存的使用情况,当缓存即将过期时,异步更新缓存,延长过期时间;
2、Go语言的 singleflight 介绍
通俗的来说就是 singleflight 将相同的并发请求合并成一个请求;进而减少对下层服务的并发请求压力;
通常用于解决防止 “缓存击穿” 的应用场景;
在构建高性能的服务时,缓存是优化数据库压力和提高响应速度的关键技术;使用缓存也会带来一些问题,其中就包括缓存击穿,它不仅会导致数据库压力剧增,引起数据库性能的下降,严重时甚至会击垮数据库,导致数据库不可用;
在 Go 语言中,golang.org/x/sync/singleflight 包提供了一种机制,确保对于任何特定 key 的并发请求在同一时刻只执行一次;这个机制有效地防止了缓存击穿问题;
本文将深入探讨 Go 语言中 singleflight 包的使用;从缓存击穿问题的基础知识开始,进而详细介绍 singleflight 包的使用,展示如何利用它来避免缓存击穿;
3、singleflight 实例
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 30 31 32 33 34 35 36 37 38 39 40 41
|
package main
import ( "context" "fmt" "golang.org/x/sync/singleflight" "testing" "time" )
func TestSingleflightDemo(t *testing.T) { var group singleflight.Group for i := 0; i < 10; i++ { go func(i int) { key := "demo" item := i val, err, _ := group.Do(key, func() (interface{}, error) { time.Sleep(time.Second * 2) return fmt.Sprintf("resultl_%d", item), nil }) if err != nil { fmt.Println("group do err:", err) return } fmt.Println("value:", val) }(i) } time.Sleep(time.Second * 5) fmt.Println("all task finished") }
|
结合 context 超时控制
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
package main
import ( "context" "fmt" "golang.org/x/sync/singleflight" "testing" "time" )
func TestSingleflightTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) go doTask(ctx) time.Sleep(time.Second * 2) cancel() }
func doTask(ctx context.Context) { var g singleflight.Group key := "demo" result := g.DoChan(key, func() (interface{}, error) { time.Sleep(time.Second * 5) return "result", nil }) select { case r := <-result: fmt.Println("result:", r.Val) case <-ctx.Done(): fmt.Println("done") return } }
|
4、singleflight 源码分析
golang.org/x/sync/singleflight singleflight 的结构体:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| type call struct { wg sync.WaitGroup val interface{} err error dups int chans []chan<- Result }
type Group struct { mu sync.Mutex m map[string]*call }
type Result struct { Val interface{} Err error Shared bool }
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() if e, ok := c.err.(*panicError); ok { panic(e) } else if c.err == errGoexit { runtime.Goexit() } 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) DoChan(key string, fn func() (interface{}, error)) <-chan Result { ch := make(chan Result, 1) g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } if c, ok := g.m[key]; ok { c.dups++ c.chans = append(c.chans, ch) g.mu.Unlock() return ch } c := &call{chans: []chan<- Result{ch}} c.wg.Add(1) g.m[key] = c g.mu.Unlock() go g.doCall(c, key, fn) return ch }
func (g *Group) Forget(key string) { g.mu.Lock() delete(g.m, key) g.mu.Unlock() }
|
5、小结
本文首先介绍了缓存击穿的概念、以及常见的缓存击穿解决方案、接着简单介绍singleflight包的实现原理;
再通过两个实例来展示singleflight包的使用方式;
重点介绍了singleflight 的源码剖析,该包的主要作用是:用于防止重复的请求,它确保给定的 key,函数在同一时间内只执行一次,多个请求共享同一结果;singleflight 能实现这种效果,关键点在于
1、将多个相同请求合并成一个请求,确保函数只执行一次singleflight 为了解决这个问题,引入了互斥锁 sync.Mutex 和 map;
2、结果共享机制singleflight通过阻塞式和非阻塞式两种方式,实现了结果的共享;