大家好,我是「Go学堂」的渔夫子,今天跟大家聊聊在我们项目中的优先级队列的实现及应用场景。
优先级队列概述
队列,是数据结构中实现先进先出策略的一种数据结构。而优先队列则是带有优先级的队列,即先按优先级分类,然后相同优先级的再 进行排队。优先级高的队列中的元素会优先被消费。如下图所示:
图1-优先级队列概况图.png
在Go中,可以定义一个切片,切片的每个元素代表一种优先级队列,切片的索引顺序代表优先级顺序,后面代码实现部分我们会详细讲解。
为什么需要优先级队列
先来看现实生活中的例子。银行的办事窗口,有普通窗口和vip窗口,vip窗口因为排队人数少,等待的时间就短,比普通窗口就会优先处理。同样,在登机口,就有贵宾通道和普通,同样贵宾通道优先登机。
在互联网中,当然就是请求和响应。使用优先级队列的作用是将请求按特定的属性划分出优先级,然后按优先级的高低进行优先处理。在研发服务的时候这里有个隐含的约束条件就是服务器资源(CPU、内存、带宽等)是有限的。如果服务器资源是无限的,那么也就不需要队列进行排队了,来一个请求就立即处理一个请求就好了。所以,为了在最大限度的利用服务器资源的前提下,将更重要的任务(优先级高的请求)优先处理,以更好的服务用户。
对于请求优先级的划分可以根据业务的特点根据价值高的优先原则来进行划分即可。例如可以根据是否是否是会员、是否是VIP会员等属性进行划分优先级。也可以根据是否是付费用户进行划分。在博客的业务中,也可以根据是否是大V的属性进行优先级划分。在互联网广告业务中,可以根据广告位资源价值高低来划分优先级。
优先级队列实现原理
01 四个角色
在完整的优先级队列中有四个角色,分别是优先级队列、工作单元、消费者worker、通知channel。
- 工作单元Job:队列里的元素。我们把每一次业务处理都封装成一个工作单元,该工作单元会进入对应的优先级队列进行排队,然后等待消费者worker来消费执行。
- 优先级队列:按优先级划分的队列,用来暂存对应优先级的工作单元Job,相同优先级的工作单元会在同一个队列里。
- noticeChan通道:当有工作单元进入优先级队列排队后,会在通道里发送一个消息,以通知消费者worker从队列中获取元素(工作单元)进行消费。
- 消费者worker:监听noticeChan,当监听到noticeChan有消息时,说明队列中有工作单元需要被处理,优先从高优先级队列中获取元素进行消费。
02 队列-消费者模式
根据队列个数和消费者个数,我们可以将队列-消费者模式分为单队列-单消费者模式、多队列(优先级队列)- 单消费者模式、多队列(优先级队列)- 多消费者模式。
我们先从最简单的单队列-单消费者模式实现,然后一步步演化成多队列(优先级队列)-多消费者模式。
03 单队列-单消费者模式实现
图2-单消费者模式.png
3.1 队列的实现
我们先来看下队列的实现。这里我们用Golang中的List数据结果来实现,List数据结构是一个双向链表,包含了将元素放到链表尾部、将头部元素弹出的操作,符合队列先进先出的特性。
好,我们看下具体的队列的数据结构:
type JobQueue struct {
mu sync.Mutex //队列的操作需要并发安全
jobList *list.List //List是golang库的双向队列实现,每个元素都是一个job
noticeChan chan struct{} //入队一个job就往该channel中放入一个消息,以供消费者消费
}
- 入队操作
/**
* 队列的Push操作
*/
func (queue *JobQueue) PushJob(job Job) {
queue.jobList.PushBack(job) //将job加到队尾
queue.noticeChan <- struct{}{}
}
到这里有同学就会问了,为什么不直接将job推送到Channel中,然后让消费者依次消费不就行了么?是的,单队列这样是可以的,因为我们最终目标是为了实现优先级的多队列,所以这里即使是单队列,我们也使用List数据结构,以便后续的演变。
还有一点,大家注意到了,这里入队操作时有一个 这样的操作:
queue.noticeChan <- struct{}{}
消费者监听的实际上不是队列本身,而是通道noticeChan。当有一个元素入队时,就往noticeChan通道中输入一条消息,这里是一个空结构体,主要作用就是通知消费者worker,队列里有要处理的元素了,可以从队列中获取了。 这个在后面演化成多队列以及多消费者模式