這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
引子
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{}
如果您有更好的方案歡迎交流與分享。
內容為作者原創,未經允許請勿轉載,謝謝合作。
關於作者:
Jesse,目前在Joygenio工作,從事golang語言開發與架構設計。
正在開發維護的產品:www.botposter.com