Go实现API并发请求的案例

生来就可爱ヽ(ⅴ<●) 提交于 2020-10-02 08:43:35

场景:

    有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
}

可以说很细致,明确的演示了整个程序的运行过程,如果觉得乱,可将演示代码删除。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!