Go -- 并发编程的两种限速方法

引子

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{}
时间: 2024-11-13 13:35:26

Go -- 并发编程的两种限速方法的相关文章

[编程题] 两种排序方法

小易喜欢的单词具有以下特性:1.单词每个字母都是大写字母2.单词没有连续相等的字母3.单词没有形如“xyxy”(这里的x,y指的都是字母,并且可以相同)这样的子序列,子序列可能不连续.例如:小易不喜欢"ABBA",因为这里有两个连续的'B'小易不喜欢"THETXH",因为这里包含子序列"THTH"小易不喜欢"ABACADA",因为这里包含子序列"AAAA"小易喜欢"A","AB

JavaScript强化教程——DOM编程(两种控制div移动的方法)

本文为H5EDU机构官方HTML5培训教程,主要介绍:JavaScript强化教程--DOM编程(两种控制div移动的方法) 第一种 按钮控制首先 创建两个html按钮和一个div并给div一个样式 input type="button" value="左" id="1"> <input type="button" value="右" id="2"> <div i

两种排序方法 网易2017内推编程题

考拉有n个字符串字符串,任意两个字符串长度都是不同的.考拉最近学习到有两种字符串的排序方法: 1.根据字符串的字典序排序.例如: "car" < "carriage" < "cats" < "doggies < "koala" 2.根据字符串的长度排序.例如: "car" < "cats" < "koala" < &

构造并发程序的三种基本方法和优缺点

构造并发程序的三种基本方法 进程 用这种方法,每个逻辑控制流都是一个进程,由内核来调度维护.因为进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信机制. I/O多路复用 在这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们自己的逻辑流.逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态.因为程序是一个单独的进程,所以所有的流都共享同一个地址空间. 假设要求编写一个echo服务器,它也能对用户从标准输入键入的交互命令做出响

浅析C# 异步编程的两种方式

一.传统BeginInvoke方式. BeginInvoke方法用于启动c#异步调用.它返回IasyncResult,可用于监视调用进度.EndInvoke方法用于检索c#异步调用结果. 调用BeginInvoke后可随时调用EndInvoke方法;如果C#异步调用未完成,EndInvoke将一直阻塞到C#异步调用完成. 总结其使用大体分5个步骤: 1.声明委拖 2.创建异步方法 3.实例化委拖(把委拖与方法关联)  A 4.通过实例的BeginInvoke调用异步方法 5.通过实例的EndIn

【原】ios打包ipa的两种实用方法(.app转.ipa)

总结一下,目前.app包转为.ipa包的方法有以下几种: 1.Apple推荐的方式,即实用xcode的archive功能 Xcode菜单栏->Product->Archive->三选一,一般选后两个. 局限性:个人开发一般采用这种方法,但是当一个证书多人使用时就稍显麻烦.一般多人开发时都是采用provisioning profile+P12文件来进行真机调试.上述方法在最后导出ipa包时需要输入appleID,这时还要向团队的其他人要.采用provisioning profile+P12

基于Apache+Tomcat负载均衡的两种实现方法

Apache+Tomcat实现负载均衡的两种实现方法 如果我们将工作在不同平台的apache能够实现彼此间的高效通信,因此它需要一种底层机制来实现--叫做apr Apr的主要目的就是为了其能够让apache工作在不同的平台上,但在linux上安装apache的时候通常都是默认安装的 [[email protected] ~]#rpm -qi aprName                 :apr                                        Relocation

OGG的Director web hang住的两种解决方法

OGG的Director web hang住的两种解决方法: OGG的Director web hang住的解释:是指web界面能登陆进去,但是看得刷新日期是很久之前的日期,并且该日期不变化. OGG的Director web hang住 的情况之一: 参考如下的mos文章: Director web displaying "Error 500-Internal Server Error". Domain log has Cannot open paging store. (Doc I

git两种合并方法 比较merge和rebase

18:01 2015/11/18git两种合并方法 比较merge和rebase其实很简单,就是合并后每个commit提交的id记录的顺序而已注意:重要的是如果公司用了grrit,grrit不允许用merge,所以好像都是用rebase却别讲解,比如:在服务器上的develop分支有多人在开发,你们同时clone或pull下来最新代码,但是开发进度不一样,你在开发一个任务的时候其他人提交了编号为1,2的commit和push,你现在开发完了也要提交,你的提交编号是3,4(注意:编号不代表顺序现实