Go基于优先级队列实现业务接口的等级划分

78 阅读4分钟

需求场景:

已知请求的第三方tts接口qps是200,业务要求下无法增加配额

现在在服务中需要实现一个新接口newtts,这个新接口负责对tts接口接口进行封装调用,也就是新接口的qps也需要是200限制 现在这个新接口有2个业务调用方c和b,c的服务请求量小,但是服务比较重要,尽量不失败, b服务请求量大,但是可以重试,有哪些方案需要考虑?尽量保证接口 qps不被浪费也不被限制 使用go 实现,新接口需要尽可能实时的响应出tts接口的查询数据?

分布式限流策略先不考虑,其他情况如何考虑

大致方案:

1.对业务调用方c和B按照优先级设置不同的权重,c的权重设置高点,b低点,每次请求newtts接口时都要把weight参数带过来

2.提前实现一个优先级队列,然后newtts接口需要实现两部分逻辑,第一部分.生成一个请求唯一id,把每次的新请求都实时的赛进队列中push操作, 第二部分,不停的监听对应的唯一id产生的tts结果(在chan或者map中),一旦有结果就中断循环,实时响应

3.针对上面第二部分的唯一id产生的tts结果是需要起一个协程不断地消费队列里的请求数据,一旦队列有数据就发起对三方tts接口请求,有数据返回就赛进一个chan或者map中,为了数据尽快响应,可以一条队列数据发起一个协程,pop操作

基于container/heap优先级队列实现

package priority_queue

import (
	"container/heap"
	"sync"
)

// 优先级队列的元素,优先级越高,数值越小
type Item struct {
	RequestID string                 // 唯一标识请求的ID
	Weight    int32                  // 优先级,数值越小优先级越高
	Metadata  map[string]interface{} // 请求的元数据
}

// RequestQueue 实现了 heap.Interface 并且是线程安全的。
type PriorityQueue struct {
	lock  sync.Mutex
	items []*Item
}

// NewPriorityQueue 创建一个新的优先级队列实例
func NewPriorityQueue() *PriorityQueue {
	pq := &PriorityQueue{}
	heap.Init(pq)
	return pq
}

// Len 实现了 heap.Interface 的 Len 方法
func (pq *PriorityQueue) Len() int {
	pq.lock.Lock()
	defer pq.lock.Unlock()
	return len(pq.items)
}

// Less 实现了 heap.Interface 的 Less 方法,用于比较优先级
func (pq *PriorityQueue) Less(i, j int) bool {
	return pq.items[i].Weight > pq.items[j].Weight
}

// Swap 实现了 heap.Interface 的 Swap 方法,用于交换元素位置
func (pq *PriorityQueue) Swap(i, j int) {
	pq.lock.Lock()
	defer pq.lock.Unlock()
	pq.items[i], pq.items[j] = pq.items[j], pq.items[i]
}

// Push 实现了 heap.Interface 的 Push 方法,用于向队列添加元素
func (pq *PriorityQueue) Push(x interface{}) {
	pq.lock.Lock()
	defer pq.lock.Unlock()
	item := x.(*Item)
	pq.items = append(pq.items, item)
}

// Pop 实现了 heap.Interface 的 Pop 方法,用于弹出优先级最高的元素
func (pq *PriorityQueue) Pop() interface{} {
	pq.lock.Lock()
	defer pq.lock.Unlock()
	old := pq.items
	n := len(old)
	item := old[n-1]
	pq.items = old[0 : n-1]
	return item
}

// Push 添加一个元素到优先级队列中
func (pq *PriorityQueue) Enqueue(requestId string, weight int32, metadata map[string]interface{}) {
	item := &Item{
		RequestID: requestId,
		Weight:    weight,
		Metadata:  metadata,
	}
	pq.Push(item)
}

// Dequeue 弹出优先级最高的元素,如果队列为空则返回错误
func (pq *PriorityQueue) Dequeue() (*Item, error) {
	// if pq.Len() == 0 {
	// 	return nil, fmt.Errorf("dequeue from an empty priority queue")
	// }
	// pq.lock.Lock()
	// defer pq.lock.Unlock()
	item := pq.Pop().(*Item)
	return item, nil
}


//------

var once sync.Once
var instance *PriorityQueue // 使用instance变量来存储单例实例

func TtsReqQueue() *PriorityQueue {
	once.Do(func() {
		instance = NewPriorityQueue() // 只在第一次调用时初始化
	})
	return instance
}


业务逻辑

func (s *ApiService) TTs(ctx context.Context, req *pb.TtsReq) (*pb.TtsReply, error) {
	requestID := uuid.New().String()
	s.speech.PushReqQueue(ctx, req.GetTxt(), req.GetIsContact(), req.GetLevel(), req.GetTraceId(), requestID)

	//等待查询结果或超时
	var ttsRes biz.TtsResult
	// 设置超时时间
	timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer timeoutCancel()

	for {
		// 因为消费队列是多协程处理,单纯使用chan会导致处理结果错乱
		// 检查ResultsMap是否有结果
		var ok bool
		value, ok := biz.ResultsMap.Load(requestID)
		if ok {
			ttsRes = value.(biz.TtsResult)   // 类型断言,因为 sync.Map 存储的是空接口类型
			biz.ResultsMap.Delete(requestID) // 删除已经取出的结果,避免内存泄漏
			break
		}
		// 使用 time.After 替代 time.Sleep,以便在等待时也能响应上下文的取消或超时
		select {
		case <-time.After(time.Millisecond * 80):
		case <-timeoutCtx.Done():
			// 超时或者上下文被取消
			if errors.Is(timeoutCtx.Err(), context.DeadlineExceeded) {
				return nil, fmt.Errorf("TTS failed: timeout waiting for result")
			}
			return nil, fmt.Errorf("TTS failed: %w", timeoutCtx.Err())
		}
	}

	if ttsRes.Err != nil {
		return nil, ttsRes.Err
	}
	ttsData := ttsRes.TtsData
	if ttsData.Info.AudioURL == "" {
		return nil, errors.New(500, "TTS_FAIL", "TTS failed")
	}
	return &pb.TtsReply{xxxx}

进队列部分

// 请求进队列,,优先级队列是全局唯一的,所以不需要传入,通过s.WaitForResult单独起个协程可加快处理速度,如果使用启动异步守护队列会导致消费不及时,影响相应时间
func (s *SpeechUseCase) PushReqQueue(ctx context.Context, txt string, level int32, traceID string, requestID string) {
	metaData := map[string]interface{}{
		"txt":        txt,
		"trace_id":   traceID,
	}
	ttsPriorityQueue.TtsReqQueue().Enqueue(requestID, level, metaData)
	s.WaitForResult(ctx)
}

消费队列,请求接口

var ResultsMap = sync.Map{}

func (c *SpeechUseCase) WaitForResult(ctx context.Context) error {

	if ttsPriorityQueue.TtsReqQueue().Len() == 0 {
		return nil
	}
	n := ttsPriorityQueue.TtsReqQueue().Len()
	var eg errgroup.Group
	for i := 0; i < n; i++ {
		eg.Go(func() error {
			start := time.Now()
			defer func() {
				if err := recover(); err != nil {
					c.log.WithContext(ctx).Errorf("WaitForResult panic:%+v,stack=%+v", err, string(debug.Stack()))
				}
			}()
			if ttsPriorityQueue.TtsReqQueue().Len() > 0 {
				request, err := ttsPriorityQueue.TtsReqQueue().Dequeue()
				if err != nil {
					c.log.WithContext(ctx).Errorf("WaitForResult Dequeue error: %v", err)
					return err
				}
				res, err := c.TTS(ctx, cast.ToString(request.Metadata["txt"]), request.Weight, cast.ToString(request.Metadata["trace_id"])) // TTS
				ttsRes := TtsResult{
					TtsData:   res,
					Err:       err,
					RequestID: request.RequestID,
				}
				ResultsMap.Store(request.RequestID, ttsRes)
				startDiff := time.Since(start)
				fmt.Printf("WaitForResult_time:%+v,queue_len:%v,weight:%+v\n", startDiff, n, request.Weight)
			}
			return nil
		})
	}
	if err := eg.Wait(); err != nil {
		return err
	}
	return nil
}