1. 程式人生 > >golang併發程式設計的兩種限速方法

golang併發程式設計的兩種限速方法

引子

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 main

import (
    "sync"
    "time"
)

//LimitRate 限速
type LimitRate struct {
    rate     int
    begin    time.Time
    count    int
    lock     sync.Mutex
}

//Limit Limit
func (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 main

import (
    "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 main

import (
    "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 設定Rate
func (l *LimitRate) SetRate(r int) {
    l.rate = r
    l.interval = time.Microsecond * time.Duration(1000*1000/l.Rate)
}

//GetRate 獲取Rate
func (l *LimitRate) GetRate() int {
    return l.rate 
}

測試

package main

import (
    "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{}

如果您有更好的方案歡迎交流與分享。

內容為作者原創,未經允許請勿轉載,謝謝合作。

關於作者:
Jesse,目前在Joygenio工作,從事golang語言開發與架構設計。
正在開發維護的產品:www.botposter.com