golang提供了goroutine快速實現併發編程,在實際環境中,如果goroutine中的代碼要消耗大量資源時(CPU、記憶體、帶寬等),我們就需要對程式限速,以防止goroutine將資源耗盡。下麵我們討論兩種對goroutine限速的實現方法。 ...
引子
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