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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM