Go singleflight

JunChow520 · · 739 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

  • 缓存穿透、缓存击穿、缓存雪崩解决方案

缓存处理

缓存处理流程

缓存击穿

单飞

缓存击穿是指缓存中没有但数据库存在的对应key的值,由于缓存中key的过期时间到期,转而去数据库读取。当并发请求多大时会引发数据库压力,瞬间倍增造成崩溃。

缓存击穿是由于对于设置过期时间的key在某时刻被超高并发地访问形成“热点”,请求全部转发到数据库造成数据库压垮。

并发访问

  • 使用Golang的mapWaitGroup特性实现并发控制

例如:并发获取数据,先尝试从缓存获取数据,若缓存不存在则从数据库获取数据。

$ vim main.go
package main

import (
    "errors"
    "example/core"
    "fmt"
    "sync"
)

var (
    wg            sync.WaitGroup
    sf            core.SingleFlight
    errorNotExist = errors.New("not exist")
)

//get DataFromDB 从数据库获取数据
func getDataFromDB(key string, i int) (string, error) {
    fmt.Printf("worker %v: get data from database...\n", i)
    return "db data success", nil
}

//getDataFromCache 从缓存中获取数据
func getDataFromCache(key string, i int) (string, error) {
    return "", errorNotExist
}

//getData 获取数据
func getData(key string, i int) (string, error) {
    //从缓存中获取数据
    data, err := getDataFromCache(key, i)
    if err != errorNotExist {
        return "", err
    } else if data != "" {
        return data, nil
    }

    //从数据库获取数据
    data, err = getDataFromDB(key, i)
    if err != nil {
        fmt.Printf("worker %v: db error: %v\n", i, err.Error())
        return "", err
    }

    return data, nil
}

func main() {
    count := 3
    wg.Add(count)
    //模拟多个并发请求
    for i := 0; i < count; i++ {
        go func(i int) {
            defer wg.Done()
            data, err := getData("key", i)
            if err != nil {
                return
            }
            fmt.Printf("worker %v: %v\n", i, data)
        }(i)
    }
    wg.Wait()
}
$ go run main.go
worker 2: get data from database...
worker 2: db data success
worker 0: get data from database...
worker 0: db data success
worker 1: get data from database...
worker 1: db data success

此时发现,3个并发请求,缓存中不存在数据,都走的是数据库。数据库瞬时会压力过大,存在崩溃的机会。

防击穿

SingleFlight(单飞)针对多个并发请求对一个失效的key进行获取源数据时,只让其他一个请求得到执行,其余均会阻塞等待执行的那个请求完毕后,将结果传递给阻塞的其他请求,达到防止击穿的效果。

  • 保护下游,针对下游的同一批请求,只有一个负责去请求,其他等待结果。
$ vim ./core/singleflight.go
package core

import "sync"

//call 表示一个正在执行或已经完成的函数调用,即需要被执行的函数。
type call struct {
    wg    sync.WaitGroup //用于阻塞调用这个call的其他请求
    value interface{}    //记录回调函数返回值
    err   error          //记录回调函数返回的错误
}

//SingleFlight 任务分组 防击穿
type SingleFlight struct {
    mutex   sync.Mutex       //为map添加互斥锁以保证并发安全
    callmap map[string]*call //对于每个需要获取的key存在一个对应的call
}

//Do 执行任务
func (sf *SingleFlight) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
    //添加互斥锁
    sf.mutex.Lock()
    //初始化
    if sf.callmap == nil {
        sf.callmap = make(map[string]*call)
    }
    //若当前key对应的回调函数正在被执行
    val, ok := sf.callmap[key]
    if ok {
        sf.mutex.Unlock() //解锁
        val.wg.Wait()     //阻塞等待执行中,等待其执行完毕后获取执行结果。
        //返回结果
        return val.value, val.err
    }

    //创建call
    obj := new(call)
    obj.wg.Add(1) //添加goroutine

    //写入map后解锁
    sf.callmap[key] = obj //写入callmap
    sf.mutex.Unlock()     //解锁

    //执行获取key的回调函数
    obj.value, obj.err = fn()
    obj.wg.Done()

    //重新上锁后删除key
    sf.mutex.Lock()
    delete(sf.callmap, key)
    sf.mutex.Unlock()

    //返回结果
    return obj.value, obj.err
}

Do方法通过传入的key和回调函数,判断若key相同则回调函数只调用一次,同时同步阻塞。

Do方法通过WaitGroup来控制

  1. 在SingleFlight结构中设置一个map,若map中的key不存在则实例化call来包含值信息,同时将key=>call的对应关系存入map,存入map时使用mutex互斥锁以保证并发安全。
  2. 如果map中已经存在key则直接执行WaitGroup.Wait()
  3. 但回调函数fn()执行完毕之后,执行WaitGroup.Done()。
  4. 此时卡在第2步的方法得以执行返回结果

缓存防穿透

为数据库访问添加并发访问保护机制以防止穿透

$ vim main.go
package main

import (
    "errors"
    "example/core"
    "fmt"
    "sync"
)

var (
    wg            sync.WaitGroup
    sf            core.SingleFlight
    errorNotExist = errors.New("not exist")
)

//get DataFromDB 从数据库获取数据
func getDataFromDB(key string, i int) (string, error) {
    fmt.Printf("worker %v: get data from database...\n", i)
    return "db data success", nil
}

//getDataFromCache 从缓存中获取数据
func getDataFromCache(key string, i int) (string, error) {
    return "", errorNotExist
}

//getData 获取数据
func getData(key string, i int) (string, error) {
    //从缓存中获取数据
    data, err := getDataFromCache(key, i)
    if err != errorNotExist {
        return "", err
    } else if data != "" {
        return data, nil
    }

    //从数据库获取数据
    val, err := sf.Do(key, func() (interface{}, error) {
        return getDataFromDB(key, i)
    })
    if err != nil {
        fmt.Printf("worker %v: db error: %v\n", i, err.Error())
        return "", err
    }
    data = val.(string)

    return data, nil
}

func main() {
    count := 3
    wg.Add(count)
    //模拟多个并发请求
    for i := 0; i < count; i++ {
        go func(i int) {
            defer wg.Done()
            data, err := getData("key", i)
            if err != nil {
                return
            }
            fmt.Printf("worker %v: %v\n", i, data)
        }(i)
    }
    wg.Wait()
}
$ go run main.go
worker 2: get data from database...
worker 2: db data success
worker 1: db data success
worker 0: db data success

请求去重

SingleFlight除了应用于缓存防击穿之外,还可以用于请求资源去重复。

package main

import (
    "example/core"
    "fmt"
    "time"
)

var (
    sf    core.SingleFlight
    chs   []chan int
    count = 3
)

//process 验证并发重复请求
func process(ch chan int, key string, index int) {
    for i := 0; i < count; i++ {
        val, err := sf.Do(key, func() (interface{}, error) {
            time.Sleep(time.Millisecond * 100)
            return "ok", nil
        })

        fmt.Printf("index=%v, i=%v, val=%v, err=%v, ch=%v\n", index, i, val, err, ch)

        got := fmt.Sprintf("%v %T", val, val)
        want := "ok string"
        if got != want {
            fmt.Printf("%v, %v\n", got, want)
            return
        }

        if err != nil {
            panic(err)
        }
    }
    ch <- 1
}

func main() {
    key := "key"
    chs = make([]chan int, count)

    for i := 0; i < count; i++ {
        ch := make(chan int)
        chs[i] = ch
        go process(ch, key, i)
    }

    for i, ch := range chs {
        <-ch
        fmt.Printf("goroutine %v quit\n", i)
    }
}

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:JunChow520

查看原文:Go singleflight

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

739 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传