在面向海量私域用户进行大促通知、服务变更提醒等大群发任务时,开发者经常面临一个严峻的挑战:频率控制。若直接使用全量高并发投递,极易因为触发底层风控频次限制而导致接口大量报错,甚至引发账号异常;而完全串行投递又无法满足大促时效性的要求。
本文将分享如何利用 Go 语言的并发特性(Goroutine)和令牌桶算法(Token Bucket),构建一个平滑限流、高可用的漏斗式群发调度器。
- 限流架构设计
我们采用"生产者-消费者"模型。群发任务被拆解为单个投递单元并推入带缓冲的 Channel。消费者群(Worker Pool)从 Channel 中读取任务,在执行底层发送前,必须向 time.Ticker 或令牌桶申请许可证,以此保证整体发送速率处于安全平滑的波形内。
- Go 语言核心实现
在这套设计中,我们调用了 QiWe API 的群发接口。由于 QiWe API 在上游已经完成了复杂的批量分发校验,我们在本地只需要控制好向其网关投递的每秒请求数(QPS)即可。、
Go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
// Task 代表单条投递任务
type Task struct {
SenderUserID string `json:"sender_userid"`
ExternalUserID string `json:"external_userid"`
Content string `json:"content"`
}
type TaskDispatcher struct {
taskChan chan Task
rateTicker *time.Ticker
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
func NewTaskDispatcher(bufferSize int, qps int) *TaskDispatcher {
ctx, cancel := context.WithCancel(context.Background())
// 按照每秒 QPS 分配令牌时间间隔
interval := time.Second / time.Duration(qps)
return &TaskDispatcher{
taskChan: make(chan Task, bufferSize),
rateTicker: time.NewTicker(interval),
ctx: ctx,
cancel: cancel,
}
}
// StartWorkers 启动指定数量的并发消费者
func (td *TaskDispatcher) StartWorkers(workerCount int) {
for i := 0; i < workerCount; i++ {
td.wg.Add(1)
go func(workerID int) {
defer td.wg.Done()
for {
select {
case <-td.ctx.Done():
return
case task, ok := <-td.taskChan:
if !ok {
return
}
// 阻塞等待限流令牌
<-td.rateTicker.C
td.executeSend(workerID, task)
}
}
}(i)
}
}
func (td *TaskDispatcher) executeSend(workerID int, task Task) {
// 调用 QiWe API 进行投递
apiURL := "https://api.qiweapi.local/v1/message/send" // 模拟网关
payload, _ := json.Marshal(map[string]interface{}{
"sender_userid": task.SenderUserID,
"external_userid": task.ExternalUserID,
"msgtype": "text",
"text": map[string]string{
"content": task.Content,
},
})
req, _ := http.NewRequestWithContext(td.ctx, "POST", apiURL, bytes.NewBuffer(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer your_token")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("[Worker %d] 网络异常: %v\n", workerID, err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Printf("[Worker %d] 成功平滑投递给客户: %s\n", workerID, task.ExternalUserID)
}
}
func main() {
// 初始化调度器:任务缓冲区 1000,限制整体 QPS 为每秒 20 次
dispatcher := NewTaskDispatcher(1000, 20)
dispatcher.StartWorkers(5) // 启动 5 个并发线程进行消费
// 模拟生成群发任务
for i := 0; i < 50; i++ {
dispatcher.taskChan <- Task{
SenderUserID: "Agent_01",
ExternalUserID: fmt.Sprintf("wm_user_id_%03d", i),
Content: "您好,这是大促期间为您精准推送的福利信息。",
}
}
close(dispatcher.taskChan)
dispatcher.wg.Wait()
dispatcher.rateTicker.Stop()
fmt.Println("全量群发任务平滑执行完毕")
}
- 架构优势
通过本地的令牌桶策略,结合 QiWe API 在接收端的智能路由,开发团队可以用极少的系统资源,构建起一条既具备大吞吐量、又绝不触碰风控红线的精细化营销分发管道。