需求:第三方的接口,限制接口请求的QPS,每秒5次
需要控制job「访问接口」的次数,
每秒不能同时超过5次
,包括 进行中的任务、刚启动的任务
要确保单位时间内(例如每秒)运行的任务数量不超过特定的上限(如5个任务),并且在任务执行完成得很快时,考虑已完成的任务和正在执行的任务作为正在运行的任务总数
,可以使用限流器来控制任务的启动频率,并结合使用信号量来管理同时运行的任务数量。
具体来说,使用一个信号量来限制同时进行的任务数量,并且在任务完成时,仅在下一秒钟允许新的任务开始,以确保即使某些任务快速完成,也不会在同一秒钟内启动超过限制数量的任务。
go
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
"golang.org/x/time/rate"
)
func RateLimit() {
const maxJobsPerSecond = 5
const numJobs = 22
var wg sync.WaitGroup
// 计数器
var runningJobs int32 // 当前正在执行的任务数量
var startedJobs int32 // 启动后的任务数量
var finishedJobs int32 // 刚完成的任务数量
limiter := rate.NewLimiter(rate.Every(time.Second/time.Duration(maxJobsPerSecond)), maxJobsPerSecond)
semaphore := make(chan struct{}, maxJobsPerSecond)
for i := 1; i <= numJobs; i++ {
wg.Add(1)
go func(jobID int) {
defer wg.Done()
limiter.Wait(context.Background()) // 等待限流器允许进行下一个任务
semaphore <- struct{}{} // 获取信号量
atomic.AddInt32(&startedJobs, 1)
atomic.AddInt32(&runningJobs, 1)
executeJob(jobID) // 执行任务
atomic.AddInt32(&finishedJobs, 1)
atomic.AddInt32(&runningJobs, -1)
<-time.After(time.Second) // 等待一秒钟后释放信号量
<-semaphore
// 打印当前状态
printStatus(&runningJobs, &startedJobs, &finishedJobs)
}(i)
}
wg.Wait()
fmt.Println("所有工作完成")
}
注意事项
- 限流器
rate.NewLimiter
用于控制任务启动的频率,以确保每秒不超过maxJobsPerSecond
个任务开始执行。 - 使用信号量
semaphore
来控制同时进行的任务数量。 - 为了确保在任何一秒内同时进行的任务数量不超过限制,在任务完成后等待一秒钟,然后再释放信号量。
这样做可以保证即使任务很快完成,也不会立即启动新的任务。
这种实现方式确保了即使任务执行得很快,每秒钟启动的新任务数量也不会超过限制,并且同时考虑了正在执行和刚刚完成的任务。
动态创建协程
-
协程的启动是动态的。在代码中,每个任务对应于一个动态创建的协程。这些协程是在循环中根据任务数量(
numJobs
)动态生成的。 -
具体来说,每当有一个新的任务需要执行时,都会创建一个新的协程来处理这个任务。这是通过在
main
函数的循环中调用go
关键字实现的。这个过程在每次循环迭代中发生,从而为每个任务动态创建一个新的协程。 -
由于使用了限流器(
rate.Limiter
),这些协程不是一次性全部创建,而是根据限流器允许的速率逐个创建。每个协程在开始执行任务之前会等待限流器的许可,以此确保每秒启动的任务数量不超过设定的最大值。
go
func executeJob(jobID int) {
startTime := time.Now() // 记录任务开始时间
// 模拟任务执行时间
fmt.Printf("%v Job %d started\n",time.Now().Format("2006-01-02 15:04:05.000"), jobID)
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())
// 随机生成一个时间间隔(例如,1到5000毫秒之间)
min := 1
max := 5000
duration := time.Duration(rand.Intn(max-min+1)+min) * time.Millisecond
time.Sleep(duration)
durationCost := time.Since(startTime) // 计算任务耗时
fmt.Printf("%v Job %d finished Cost:%v\n", time.Now().Format("2006-01-02 15:04:05.000"),jobID, durationCost)
}
func printStatus(runningJobs, startedJobs, finishedJobs *int32) {
fmt.Printf("Current status - Running: %d, Started: %d, Finished: %d\n",
atomic.LoadInt32(runningJobs),
atomic.LoadInt32(startedJobs),
atomic.LoadInt32(finishedJobs))
}
可以在代码中添加额外的逻辑来跟踪和打印正在执行、进行中、刚启动和刚完成的任务数量。使用原子操作(来自 sync/atomic
包)来确保在并发环境下对这些计数器的操作是安全的。
在这个示例中:
- 使用
sync/atomic
包中的AddInt32
和LoadInt32
来安全地增加和读取计数器的值。 - 在每个任务开始时,增加
startedJobs
和runningJobs
计数器。 - 在每个任务完成时,增加
finishedJobs
计数器,并减少runningJobs
计数器。 - 在任务完成后和释放信号量前,打印当前的任务状态。
注意事项
- 这种方法可以帮助我们跟踪不同状态下的任务数量。
- 使用原子操作确保在并发环境中对计数器的读写是安全的。
printStatus
函数在每个任务的结束时被调用,以打印当前的任务状态。