引子
golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU、内存、带宽等),我们就需要对程序限速,以防止goroutine将资源耗尽。
以下面伪代码为例,看看goroutine如何拖垮一台DB。假设userList长度为10000,先从数据库中查询userList中的user是否在数据库中存在,存在则忽略,不存在则创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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) } } 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的速度,这里我们讨论两种通用的方法。
方案一
在限速时,一种方案是丢弃请求,即请求速度太快时,对后进入的请求直接抛弃。
实现
实现逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| package main import ( "sync" "time" ) type LimitRate struct { rate int begin time.Time count int lock sync.Mutex } func (l *LimitRate) Limit() bool { result := true l.lock.Lock() if l.count == l.rate { if time.Now().Sub(l.begin) >= time.Second { l.begin = time.Now() l.count = 0 } else { result = false } } else { l.count++ } l.lock.Unlock() return result } func (l *LimitRate) SetRate(r int) { l.rate = r l.begin = time.Now() } func (l *LimitRate) GetRate() int { return l.rate }
|
测试
下面是测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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!") } wg.Done() }() } wg.Wait() }
|
运行结果
只显示3次Got it!,说明另外7次Limit返回的结果为false。限速成功。
方案二
在限速时,另一种方案是等待,即请求速度太快时,后到达的请求等待前面的请求完成后才能运行。这种方案类似一个队列。
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| type LimitRate struct { rate int interval time.Duration lastAction time.Time lock sync.Mutex } 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) } } func (l *LimitRate) SetRate(r int) { l.rate = r l.interval = time.Microsecond * time.Duration(1000*1000/l.Rate) } func (l *LimitRate) GetRate() int { return l.rate }
|
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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)) }
|
运行结果
1 2 3 4 5 6 7 8 9 10 11
| 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次。限速成功。
改造
回到最初的例子中,我们将限速功能加进去。这里需要注意,我们的例子中,请求是不能被丢弃的,只能排队等待,所以我们使用方案二的限速方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| var lr LimitRate lr.SetRate(20) 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