需求:第三方的接口,限制接口请求的QPS,每秒5次
需要控制job「访问接口」的次数,
每秒不能同时超过5次,包括 进行中的任务、刚启动的任务
要确保单位时间内(每秒)运行的任务数量不超过上限(如5个任务),并且在任务执行完成得很快时,考虑已完成的任务和正在执行的任务作为正在运行的任务总数,可以使用限流器来控制任务的启动频率,并结合使用信号量来管理同时运行的任务数量。
限制的是Job任务数量「访问接口QPS」,不是协程每秒新启动数量;**因为协程完成前,可以同时处理多个Job任务**;每秒协程数量的启动,取决于任务执行的速度以及每秒内任务「QPS」的总数的上限
具体来说,使用一个信号量来限制同时进行的任务数量,并且在任务完成时,仅在下一秒钟允许新的任务开始,以确保即使某些任务快速完成,也不会在同一秒钟内启动超过限制数量的任务。
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),这些协程不是一次性全部创建,而是根据限流器允许的速率逐个创建。每个协程在开始执行任务之前会等待限流器的许可,以此确保每秒启动的任务数量不超过设定的最大值。
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函数在每个任务的结束时被调用,以打印当前的任务状态。
执行结果:
符合预期:
即便在设置,最小1毫秒、最大15毫秒job随机执行耗时的边界情况下,
同时新增和进行中的job不会超过5个
go test -v --run TestRateLimit test/rate_test.go
=== RUN TestRateLimit
2023-12-23 00:00:24.740 Job 1 started
2023-12-23 00:00:24.740 Job 22 started
2023-12-23 00:00:24.740 Job 12 started
2023-12-23 00:00:24.740 Job 13 started
2023-12-23 00:00:24.740 Job 14 started
2023-12-23 00:00:24.864 Job 14 finished Cost:123.410271ms
Current status - Running: 4, Started: 5, Finished: 1
2023-12-23 00:00:25.864 Job 15 started
2023-12-23 00:00:25.939 Job 13 finished Cost:1.199153428s
Current status - Running: 4, Started: 6, Finished: 2
2023-12-23 00:00:26.940 Job 11 started
2023-12-23 00:00:27.094 Job 22 finished Cost:2.35398201s
Current status - Running: 4, Started: 7, Finished: 3
2023-12-23 00:00:28.095 Job 16 started
2023-12-23 00:00:28.143 Job 1 finished Cost:3.402944163s
2023-12-23 00:00:28.151 Job 12 finished Cost:3.411032652s
Current status - Running: 3, Started: 8, Finished: 5
2023-12-23 00:00:29.144 Job 17 started
Current status - Running: 4, Started: 9, Finished: 5
2023-12-23 00:00:29.152 Job 20 started
2023-12-23 00:00:29.487 Job 20 finished Cost:335.311619ms
2023-12-23 00:00:30.025 Job 15 finished Cost:4.160265608s
Current status - Running: 3, Started: 10, Finished: 7
2023-12-23 00:00:30.487 Job 19 started
2023-12-23 00:00:30.580 Job 11 finished Cost:3.640324769s
2023-12-23 00:00:30.674 Job 16 finished Cost:2.579614128s
2023-12-23 00:00:31.004 Job 17 finished Cost:1.860291361s
Current status - Running: 1, Started: 11, Finished: 10
2023-12-23 00:00:31.025 Job 21 started
2023-12-23 00:00:31.561 Job 21 finished Cost:535.634739ms
Current status - Running: 1, Started: 12, Finished: 11
2023-12-23 00:00:31.581 Job 2 started
Current status - Running: 2, Started: 13, Finished: 11
2023-12-23 00:00:31.675 Job 3 started
Current status - Running: 3, Started: 14, Finished: 11
2023-12-23 00:00:32.004 Job 4 started
Current status - Running: 4, Started: 15, Finished: 11
2023-12-23 00:00:32.561 Job 5 started
2023-12-23 00:00:32.779 Job 5 finished Cost:218.317466ms
2023-12-23 00:00:32.888 Job 3 finished Cost:1.21257771s
2023-12-23 00:00:33.739 Job 2 finished Cost:2.158699311s
Current status - Running: 2, Started: 16, Finished: 14
2023-12-23 00:00:33.780 Job 8 started
2023-12-23 00:00:33.820 Job 19 finished Cost:3.332687292s
Current status - Running: 2, Started: 17, Finished: 15
2023-12-23 00:00:33.888 Job 6 started
2023-12-23 00:00:34.096 Job 6 finished Cost:208.330622ms
Current status - Running: 2, Started: 18, Finished: 16
2023-12-23 00:00:34.740 Job 7 started
Current status - Running: 3, Started: 19, Finished: 16
2023-12-23 00:00:34.820 Job 18 started
2023-12-23 00:00:34.894 Job 7 finished Cost:154.319293ms
Current status - Running: 3, Started: 20, Finished: 17
2023-12-23 00:00:35.096 Job 9 started
Current status - Running: 4, Started: 21, Finished: 17
2023-12-23 00:00:35.895 Job 10 started
2023-12-23 00:00:35.922 Job 9 finished Cost:826.052404ms
2023-12-23 00:00:36.002 Job 8 finished Cost:2.221948228s
2023-12-23 00:00:36.835 Job 4 finished Cost:4.83101777s
2023-12-23 00:00:36.855 Job 10 finished Cost:959.314167ms
Current status - Running: 1, Started: 22, Finished: 21
Current status - Running: 1, Started: 22, Finished: 21
Current status - Running: 1, Started: 22, Finished: 21
Current status - Running: 1, Started: 22, Finished: 21
2023-12-23 00:00:38.451 Job 18 finished Cost:3.631147701s
Current status - Running: 0, Started: 22, Finished: 22
所有工作完成
--- PASS: TestRateLimit (14.71s)
PASS
ok command-line-arguments 14.714s