首页 > 代码库 > Go -- 并发编程的两种限速方法

Go -- 并发编程的两种限速方法

引子

golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU、内存、带宽等),我们就需要对程序限速,以防止goroutine将资源耗尽。以下面伪代码为例,看看goroutine如何拖垮一台DB。假设userList长度为10000,先从数据库中查询userList中的user是否在数据库中存在,存在则忽略,不存在则创建。

//不使用goroutine,程序运行时间长,但数据库压力不大for _,v:=range userList {    user:=db.user.Get(v.ID)    if user==nil {        newUser:=user{ID:v.ID,UserName:v.UserName}        db.user.Insert(newUser)    }}//使用goroutine,程序运行时间短,但数据库可能被拖垮for _,v:=range userList {    u:=v    go func(){        user:=db.user.Get(u.ID)        if user==nil {            newUser:=user{ID:u.ID,UserName:u.UserName}            db.user.Insert(newUser)        }    }()}select{}

 

在示例中,DB在1秒内接收10000次读操作,最大还会接受10000次写操作,普通的DB服务器很难支撑。针对DB,可以在连接池上做手脚,控制访问DB的速度,这里我们讨论两种通用的方法。

方案一

在限速时,一种方案是丢弃请求,即请求速度太快时,对后进入的请求直接抛弃。

实现

实现逻辑如下:

package mainimport (    "sync"    "time")//LimitRate 限速type LimitRate struct {    rate     int    begin    time.Time    count    int    lock     sync.Mutex}//Limit Limitfunc (l *LimitRate) Limit() bool {    result := true    l.lock.Lock()    //达到每秒速率限制数量,检测记数时间是否大于1秒    //大于则速率在允许范围内,开始重新记数,返回true    //小于,则返回false,记数不变    if l.count == l.rate {        if time.Now().Sub(l.begin) >= time.Second {            //速度允许范围内,开始重新记数            l.begin = time.Now()            l.count = 0        } else {            result = false        }    } else {        //没有达到速率限制数量,记数加1        l.count++    }    l.lock.Unlock()    return result}//SetRate 设置每秒允许的请求数func (l *LimitRate) SetRate(r int) {    l.rate = r    l.begin = time.Now()}//GetRate 获取每秒允许的请求数func (l *LimitRate) GetRate() int {    return l.rate}

 

测试

下面是测试代码:

package mainimport (    "fmt")func main() {    var wg sync.WaitGroup    var lr LimitRate    lr.SetRate(3)        for i:=0;i<10;i++{        wg.Add(1)            go func(){                if lr.Limit() {                    fmt.Println("Got it!")//显示3次Got it!                }                            wg.Done()            }()    }    wg.Wait()}

 

运行结果

Got it!Got it!Got it!

只显示3次Got it!,说明另外7次Limit返回的结果为false。限速成功。

方案二

在限速时,另一种方案是等待,即请求速度太快时,后到达的请求等待前面的请求完成后才能运行。这种方案类似一个队列。

实现

//LimitRate 限速type LimitRate struct {    rate       int    interval   time.Duration    lastAction time.Time    lock       sync.Mutex}

 

//Limit 限速package mainimport (    "sync"    "time")func (l *LimitRate) Limit() bool {    result := false    for {        l.lock.Lock()        //判断最后一次执行的时间与当前的时间间隔是否大于限速速率        if time.Now().Sub(l.lastAction) > l.interval {            l.lastAction = time.Now()                result = true            }        l.lock.Unlock()        if result {            return result        }        time.Sleep(l.interval)    }}//SetRate 设置Ratefunc (l *LimitRate) SetRate(r int) {    l.rate = r    l.interval = time.Microsecond * time.Duration(1000*1000/l.Rate)}//GetRate 获取Ratefunc (l *LimitRate) GetRate() int {    return l.rate }

 

测试

package mainimport (    "fmt"    "sync"    "time")func main() {    var wg sync.WaitGroup    var lr LimitRate    lr.SetRate(3)        b:=time.Now()    for i := 0; i < 10; i++ {        wg.Add(1)        go func() {            if lr.Limit() {                fmt.Println("Got it!")            }            wg.Done()        }()    }    wg.Wait()    fmt.Println(time.Since(b))}

运行结果

Got it!Got it!Got it!Got it!Got it!Got it!Got it!Got it!Got it!Got it!3.004961704s

与方案一不同,显示了10次Got it!但是运行时间是3.00496秒,同样每秒没有超过3次。限速成功。

改造

回到最初的例子中,我们将限速功能加进去。这里需要注意,我们的例子中,请求是不能被丢弃的,只能排队等待,所以我们使用方案二的限速方法。

var lr LimitRate//方案二//限制每秒运行20次,可以根据实际环境调整限速设置,或者由程序动态调整。lr.SetRate(20)//使用goroutine,程序运行时间短,但数据库可能被拖垮for _,v:=range userList {    u:=v    go func(){        lr.Limit()        user:=db.user.Get(u.ID)        if user==nil {            newUser:=user{ID:u.ID,UserName:u.UserName}            db.user.Insert(newUser)        }    }()}select{}

 

Go -- 并发编程的两种限速方法