场景:
有N 个并发请求来访问Api1时 ,如果数据库或者web服务器没有对请求做限制,那么所有请求都会访问一次数据库,很可能造成数据库压力比较大,而且 HTTP访问也比较耗时。
实现:
有N 个并发请求来访问Api1时, 只有一个请求可以访问到数据库,其他请求共享一个请求的结果。
安排:
1. 定义一个请求组,来存储所有的请求
type RequestGroup struct {
mu sync.Mutex
m map[string]*Result // 请求类型=>请求结果
}
我们使用使用Result类型来存储请求结果,mu 对请求的管理。(此处如果不清楚如何使用,后续详细讲解)
2. 定义一个请求结果的类型
type Result struct {
wg sync.WaitGroup
val interface{}
err error
}
好了,那么我们如何处理并发时来的请求呢? 所有请求的结果该如何处理呢?
首先,我们应该在有请求时,开始拦截验证是否同时有相同的请求访问,如果有,阻塞,直到第一个访问数据库的请求结束,所有请求获取到结果后结束。
代码演示:
func (g *RequestGroup ) Do (key string, getDataFunc func()(interface{}, error)) (interface{}, error) {
g.mu.Lock() // 【1】
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
g.mu.Unlock()
c.wg.Wait() // 【4】
return c.val, c.err
}
c := new(Result)
c.wg.Add(1)
g.m[key] = c
// 首个请求类型已经存储
g.mu.Unlock() // 【2】
c.val, c.err = getDataFunc()
c.wg.Done() // 【3】
g.mu.Lock()
delete(g.m, key) // 【5】每次完成将某个请求的标识删除
g.mu.Unlock()
return c.val, c.err
}
- 假设N个并发请求,请求A 进入Do方法后,N-1个请求会被阻塞在【1】处,当A 将请求类型存储时,释放请求锁,然后N-1个请求依次(通过Lock =》 Unlock)进入,同时阻塞在【4】位置。等待【3】释放,整个正常进入最终获取结果阶段。
- 程序最后要删除请求类型标识,否则下次请求进入还是缓存的数据。
验证:
模仿并发请求
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
val, err := G.Do(i, "key", func() (interface{}, error) {
time.Sleep(3 * time.Second) // 模仿数据库请求
ff++
return ff, nil
})
if err == nil {
fmt.Println(i, "获取结果...", val)
}
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("全部请求结束。。。。")
}
由于代码比较乱,为了方便演示,加入了一些标识在程序中,
完整代码如下:
package main
import (
"fmt"
"sync"
"time"
)
type Result struct {
wg sync.WaitGroup
val interface{}
err error
}
type RequestGroup struct {
mu sync.Mutex
m map[string]*Result
}
var G = &RequestGroup{
m: make(map[string]*Result),
}
var ff = 0
var wg sync.WaitGroup
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
val, err := G.Do(i, "key", func() (interface{}, error) {
time.Sleep(3 * time.Second) // 模仿数据库请求
ff++
return ff, nil
})
if err == nil {
fmt.Println(i, "获取结果...", val)
}
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("全部请求结束。。。。")
}
// 并发时,将相同key的请求wait等待第一个请求获取结果
/**
非并发期间:key 请求 req1 -> 内存中没有key-> 创建key=>val -> 释放key=>val
并发期间: key 请求 req1 -> 内存中没有key-> 创建key=>val -> 释放key=>val
*/
func (g *RequestGroup) Do(idx int, key string, getDataFunc func() (interface{}, error)) (interface{}, error) {
fmt.Println(idx, "阻塞")
g.mu.Lock() // 【1】
time.Sleep(2 * time.Second)
fmt.Println(idx, "..进入了")
if g.m == nil {
g.m = make(map[string]*Result)
}
if c, ok := g.m[key]; ok {
g.mu.Unlock()
fmt.Println(idx, "......我阻塞再次.")
c.wg.Wait() // 【4】
return c.val, c.err
}
c := new(Result)
c.wg.Add(1)
g.m[key] = c
// 首个请求类型存储
time.Sleep(3 * time.Second)
fmt.Println(idx, "....释放锁")
g.mu.Unlock() // 【2】
// 结果获取
c.val, c.err = getDataFunc()
time.Sleep(15 * time.Second)
fmt.Println(idx, "..........释放锁2")
c.wg.Done() // 【3】
// 请求标识清理
g.mu.Lock()
delete(g.m, key) // 【5】每次完成将某个请求的标识删除
g.mu.Unlock()
return c.val, c.err
}
可以说很细致,明确的演示了整个程序的运行过程,如果觉得乱,可将演示代码删除。
来源:oschina
链接:https://my.oschina.net/90design/blog/4374313