在高并发的业务场景中,如果短时间内发送大量请求到下游服务,很容易造成下游服务过载,甚至引发整个链路的服务不可用。Go语言标准库的time包中提供的Ticker类型,可以按照固定的时间间隔重复触发事件,非常适合用来实现请求限流,控制并发场景下的请求发送频率,避免对下游服务造成过大压力。

Ticker的基本工作原理
Ticker是Go语言中用于周期性触发事件的工具,创建时会指定一个时间间隔,之后每隔这个间隔就会向Ticker的C通道发送一个当前时间值。当不再需要使用Ticker时,必须调用其Stop方法释放相关资源,否则会造成资源泄漏。
下面是一个简单的Ticker使用示例,展示如何每隔固定时间打印一次日志:
package main
import (
"fmt"
"time"
)
func main() {
// 创建间隔为1秒的Ticker
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
// 运行5秒后停止
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
select {
case <-ticker.C:
fmt.Println("触发一次周期事件")
case <-timer.C:
fmt.Println("5秒时间到,退出循环")
return
}
}
}
并发请求场景下的限流实现
在实际的并发请求场景中,我们通常需要控制单位时间内发送的请求数量,比如每秒最多发送10个请求。可以结合Ticker和goroutine来实现这个需求,下面是一个完整的并发请求限流示例:
package main
import (
"context"
"fmt"
"sync"
"time"
)
// 模拟发送单个请求的函数
func sendRequest(ctx context.Context, requestID int) error {
// 模拟请求耗时
time.Sleep(100 * time.Millisecond)
fmt.Printf("请求%d发送完成n", requestID)
return nil
}
// 限流发送请求的函数,qps为每秒允许的请求数,total为总请求数
func rateLimitSend(ctx context.Context, qps int, total int) error {
// 计算每次发送请求的间隔
interval := time.Second / time.Duration(qps)
ticker := time.NewTicker(interval)
defer ticker.Stop()
var wg sync.WaitGroup
// 用于控制并发的通道,避免同时启动过多goroutine
concurrentChan := make(chan struct{}, qps)
defer close(concurrentChan)
for i := 0; i < total; i++ {
select {
case <-ctx.Done():
fmt.Println("收到取消信号,停止发送请求")
wg.Wait()
return ctx.Err()
case <-ticker.C:
// 控制并发数量
concurrentChan <- struct{}{}
wg.Add(1)
requestID := i + 1
go func(id int) {
defer wg.Done()
defer func() { <-concurrentChan }()
if err := sendRequest(ctx, id); err != nil {
fmt.Printf("请求%d发送失败: %vn", id, err)
}
}(requestID)
}
}
wg.Wait()
return nil
}
func main() {
// 创建带超时的context,总超时时间为10秒
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 设置每秒最多发送5个请求,总请求数为20个
const qps = 5
const totalRequests = 20
if err := rateLimitSend(ctx, qps, totalRequests); err != nil {
fmt.Printf("请求发送过程出错: %vn", err)
} else {
fmt.Println("所有请求发送完成")
}
}
代码逻辑解析
- 首先根据设置的QPS计算Ticker的时间间隔,比如QPS为5时,间隔为200毫秒,保证每秒最多触发5次发送逻辑。
- 使用带缓冲的通道
concurrentChan控制并发的goroutine数量,避免同时启动过多goroutine导致系统资源占用过高。 - 每次Ticker触发时,启动一个goroutine发送请求,同时等待context的取消或超时信号,确保可以优雅停止请求发送。
- 使用
sync.WaitGroup等待所有正在发送的请求完成,避免程序提前退出导致请求中断。
结合Context实现优雅退出
在实际生产环境中,可能需要支持外部触发停止限流发送的逻辑,比如服务收到关闭信号时需要停止发送新的请求,同时等待已经发起的请求完成。上面的示例中已经结合了context.WithTimeout实现了超时控制,也可以替换为context.WithCancel来支持手动取消:
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 模拟3秒后触发取消逻辑
go func() {
time.Sleep(3 * time.Second)
fmt.Println("触发手动取消")
cancel()
}()
// 启动限流发送逻辑
go func() {
// 这里省略rateLimitSend的具体实现,和之前的示例一致
// 仅展示取消逻辑的使用方式
}()
// 等待取消信号
<-ctx.Done()
fmt.Println("程序退出")
}
注意事项
- Ticker使用完成后必须调用Stop方法,否则Ticker关联的定时器资源不会被释放,会导致内存泄漏。
- 如果限流的时间精度要求较高,需要注意Go语言的定时器存在一定的时间漂移,不适合对时间精度要求极高的场景。
- 当QPS设置过高时,Ticker的间隔会非常短,此时需要考虑goroutine的调度开销,适当调整并发控制策略。
- 如果下游服务有更严格的限流规则,需要结合下游返回的限流状态码动态调整发送频率,而不是固定使用Ticker的间隔。