singleflight---防缓存穿透利器

SingleFlight

总结
  • 提供了三个参数Do同步通知Dochan异步chan通知Forget删除map的存在的键值
  • 每次调用都会重置map
  • 结构体是由mu sync.Mutexm map[string]*call组成,mu用于操作的排它锁,m用于存储已有的键值的个数
  • 如果m中存在当前键值对,就会阻塞等待,不存在的时候会加创建call结构存入m中
  • Dochan会异步执行,将结果放入chan通知其他阻塞的协程
  • 阻塞的协程永的WaitGroup实现
do 的简单使用
package main

import (
	"time"

	"golang.org/x/sync/singleflight"
	"log"
)

func main() {
	var singleSetCache singleflight.Group

	getAndSetCache := func(requestID int, cacheKey string) (string, error) {
		log.Printf("request %v start to get and set cache...", requestID)
		value, _, _ := singleSetCache.Do(cacheKey, func() (ret interface{}, err error) { //do的入参key,可以直接使用缓存的key,这样同一个缓存,只有一个协程会去读DB
			log.Printf("request %v is setting cache...", requestID)
			time.Sleep(3 * time.Second)
			log.Printf("request %v set cache success!", requestID)
			return "VALUE", nil
		})
		return value.(string), nil
	}

	cacheKey := "cacheKey"
	for i := 1; i < 10; i++ { //模拟多个协程同时请求
		go func(requestID int) {
			value, _ := getAndSetCache(requestID, cacheKey)
			log.Printf("request %v get value: %v", requestID, value)
		}(i)
	}
	time.Sleep(20 * time.Second)

	for i := 1; i < 10; i++ { //模拟多个协程同时请求
		go func(requestID int) {
			value, _ := getAndSetCache(requestID, cacheKey)
			log.Printf("request %v get value: %v", requestID, value)
		}(i)
	}

	time.Sleep(20 * time.Second)
}

2020/09/28 14:12:11 request 3 start to get and set cache...
2020/09/28 14:12:11 request 3 is setting cache...
2020/09/28 14:12:11 request 1 start to get and set cache...
2020/09/28 14:12:11 request 2 start to get and set cache...
2020/09/28 14:12:11 request 9 start to get and set cache...
2020/09/28 14:12:11 request 6 start to get and set cache...
2020/09/28 14:12:11 request 7 start to get and set cache...
2020/09/28 14:12:11 request 4 start to get and set cache...
2020/09/28 14:12:11 request 5 start to get and set cache...
2020/09/28 14:12:11 request 8 start to get and set cache...
2020/09/28 14:12:14 request 3 set cache success!
2020/09/28 14:12:14 request 3 get value: VALUE
2020/09/28 14:12:14 request 8 get value: VALUE
2020/09/28 14:12:14 request 1 get value: VALUE
2020/09/28 14:12:14 request 2 get value: VALUE
2020/09/28 14:12:14 request 9 get value: VALUE
2020/09/28 14:12:14 request 6 get value: VALUE
2020/09/28 14:12:14 request 7 get value: VALUE
2020/09/28 14:12:14 request 4 get value: VALUE
2020/09/28 14:12:14 request 5 get value: VALUE
2020/09/28 14:12:31 request 9 start to get and set cache...
2020/09/28 14:12:31 request 9 is setting cache...
2020/09/28 14:12:31 request 5 start to get and set cache...
2020/09/28 14:12:31 request 6 start to get and set cache...
2020/09/28 14:12:31 request 7 start to get and set cache...
2020/09/28 14:12:31 request 8 start to get and set cache...
2020/09/28 14:12:31 request 4 start to get and set cache...
2020/09/28 14:12:31 request 2 start to get and set cache...
2020/09/28 14:12:31 request 1 start to get and set cache...
2020/09/28 14:12:31 request 3 start to get and set cache...
2020/09/28 14:12:34 request 9 set cache success!
2020/09/28 14:12:34 request 9 get value: VALUE
2020/09/28 14:12:34 request 3 get value: VALUE
2020/09/28 14:12:34 request 4 get value: VALUE
2020/09/28 14:12:34 request 2 get value: VALUE
2020/09/28 14:12:34 request 8 get value: VALUE
2020/09/28 14:12:34 request 5 get value: VALUE
2020/09/28 14:12:34 request 7 get value: VALUE
2020/09/28 14:12:34 request 6 get value: VALUE
2020/09/28 14:12:34 request 1 get value: VALUE

Process finished with exit code 0

每一次调用do都会重新开始,内存不会持久化支持的map集合

docall 的简单使用
package main

import (
	"errors"
	"golang.org/x/sync/singleflight"
	"log"
	"time"
)

func main() {
	var singleSetCache singleflight.Group

	getAndSetCache := func(requestID int, cacheKey string) (string, error) {
		log.Printf("request %v start to get and set cache...", requestID)
		retChan := singleSetCache.DoChan(cacheKey, func() (ret interface{}, err error) {
			log.Printf("request %v is setting cache...", requestID)
			time.Sleep(3 * time.Second)
			log.Printf("request %v set cache success!", requestID)
			return "VALUE", nil
		})

		var ret singleflight.Result

		timeout := time.After(5 * time.Second)

		select { //加入了超时机制
		case <-timeout:
			log.Printf("time out!")
			return "", errors.New("time out")
		case ret = <-retChan: //从chan中取出结果
			return ret.Val.(string), ret.Err
		}
		return "", nil
	}

	cacheKey := "cacheKey"
	for i := 1; i < 10; i++ {
		go func(requestID int) {
			value, _ := getAndSetCache(requestID, cacheKey)
			log.Printf("request %v get value: %v", requestID, value)
		}(i)
	}
	time.Sleep(20 * time.Second)
}

2020/09/28 14:19:13 request 4 start to get and set cache...
2020/09/28 14:19:13 request 2 start to get and set cache...
2020/09/28 14:19:13 request 3 start to get and set cache...
2020/09/28 14:19:13 request 8 start to get and set cache...
2020/09/28 14:19:13 request 1 start to get and set cache...
2020/09/28 14:19:13 request 9 start to get and set cache...
2020/09/28 14:19:13 request 5 start to get and set cache...
2020/09/28 14:19:13 request 7 start to get and set cache...
2020/09/28 14:19:13 request 6 start to get and set cache...
2020/09/28 14:19:13 request 4 is setting cache...
2020/09/28 14:19:16 request 4 set cache success!
2020/09/28 14:19:16 request 6 get value: VALUE
2020/09/28 14:19:16 request 4 get value: VALUE
2020/09/28 14:19:16 request 2 get value: VALUE
2020/09/28 14:19:16 request 3 get value: VALUE
2020/09/28 14:19:16 request 8 get value: VALUE
2020/09/28 14:19:16 request 1 get value: VALUE
2020/09/28 14:19:16 request 9 get value: VALUE
2020/09/28 14:19:16 request 5 get value: VALUE
2020/09/28 14:19:16 request 7 get value: VALUE

Process finished with exit code 0


每一次调用dochan都会重新开始,内存不会持久化支持的map集合,只不过将结果挡在chan中进行传输

dochan源码
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)//可以看到,每个等待的协程,都有一个结果channel。从之前的g.doCall里也可以看到,每个channel都给塞了结果。为什么不所有协程共用一个channel?因为那样就得在channel里塞至少与协程数量一样的结果数量,但是你却无法保证用户一个协程只读取一次。
      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
}

singleflight 是 Go 语言扩展包中提供了另一种同步原语,这其实也是作者最喜欢的一种同步扩展机制,它能够在一个服务中抑制对下游的多次重复请求,一个比较常见的使用场景是 — 我们在使用 Redis 对数据库中的一些热门数据进行了缓存并设置了超时时间,缓存超时的一瞬间可能有非常多的并行请求发现了 Redis 中已经不包含任何缓存所以大量的流量会打到数据库上影响服务的延时和质量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmMDYyrU-1601274750210)(data/image-20200928105812664.png)]

但是 singleflight 就能有效地解决这个问题,它的主要作用就是对于同一个 Key 最终只会进行一次函数调用,在这个上下文中就是只会进行一次数据库查询,查询的结果会写回 Redis 并同步给所有请求对应 Key 的用户:

在这里插入图片描述

这其实就减少了对下游的瞬时流量,在获取下游资源非常耗时,例如:访问缓存、数据库等场景下就非常适合使用 singleflight 对服务进行优化,在上述的这个例子中我们就可以在想 Redis 和数据库中获取数据时都使用 singleflight 提供的这一功能减少下游的压力;它的使用其实也非常简单,我们可以直接使用 singleflight.Group{} 创建一个新的 Group 结构体,然后通过调用 Do 方法就能对相同的请求进行抑制:

结构体

Group 结构体本身由一个互斥锁 Mutex 和一个从 Keycall 结构体指针的映射表组成,每一个 call 结构体都保存了当前这次调用对应的信息:

type Group struct {
    mu sync.Mutex
    m  map[string]*call
}
type call struct {
    wg sync.WaitGroup
    val interface{}
    err error
    dups  int
    chans []chan<- Result
}
  • call 结构体中的 valerr 字段都是在执行传入的函数时只会被赋值一次,它们也只会在 WaitGroup 等待结束都被读取,

  • dupschans 字段分别用于存储当前 singleflight 抑制的请求数量以及在结果返回时将信息传递给调用方。

调用流程

每次 Do 方法的调用时都会获取互斥锁并尝试对 Group 持有的映射表进行懒加载,随后判断是否已经存在 key 对应的函数调用:

  • 当不存在对应的call结构体时:
    • 初始化一个新的 call 结构体指针;
    • 增加 WaitGroup 持有的计数器;
    • call 结构体指针添加到映射表;
    • 释放持有的互斥锁 Mutex
    • 阻塞地调用 doCall 方法等待结果的返回;
  • 当已经存在对应的call结构体时;
    • 增加 dups 计数器,它表示当前重复的调用次数;
    • 释放持有的互斥锁 Mutex
    • 通过 WaitGroup.Wait 等待请求的返回;
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
}

因为 valerr 两个字段都只会在 doCall 方法中被赋值,所以当 doCall 方法和 WaitGroup.Wait 方法返回时,这两个值就会返回给 Do 函数的调用者。

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
    c.val, c.err = fn()
    c.wg.Done()
    g.mu.Lock()
    delete(g.m, key)
    for _, ch := range c.chans {
        ch <- Result{c.val, c.err, c.dups > 0}
    }
    g.mu.Unlock()
}

doCall 中会运行传入的函数 fn,该函数的返回值就会赋值给 c.valc.err,函数执行结束后就会调用 WaitGroup.Done 方法通知所有被抑制的请求,当前函数已经执行完成,可以从 call 结构体中取出返回值并返回了;在这之后,doCall 方法会获取持有的互斥锁并通过管道将信息同步给使用 DoChan 方法的调用方。

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
}

DoChan 方法和 Do的区别就是,它使用 Goroutine 异步执行 doCall 并向 call 持有的 chans 切片中追加 chan Result 变量,这也是它能够提供异步传值的原因。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

a...Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值