Go combat | Implementación del procesamiento de colas de solicitudes http

En escenarios de alta concurrencia, para reducir la presión del sistema, se utiliza un mecanismo para poner en cola las solicitudes. Este artículo describe cómo se implementa en Go.

Primero, el método de procesamiento secuencial de solicitudes http

Primero, veamos la lógica normal de procesamiento de solicitudes. El cliente envía la solicitud, el servidor web recibe la solicitud, luego la procesa y finalmente responde al cliente en una lógica secuencial de este tipo. Como se muestra abajo:01-solicitud normal.png

El código se implementa de la siguiente manera:

package main

import (
	"fmt"
	"net/http"
)

func main() {

	myHandler := MyHandler{}

	http.Handle("/", &myHandler)

	http.ListenAndServe(":8080", nil)
}

type MyHandler struct {

}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello Go"))
}

Ingrese http://localhost:8080/ en el navegador para mostrar la página "Hello Go" en la página.

En circunstancias normales, cuando las personas desarrollan sistemas web, generalmente manejan las solicitudes de esta manera. A continuación, veamos cómo poner en cola las solicitudes en condiciones de alta simultaneidad.

2. Procesamiento asíncrono de solicitudes http: procesamiento en cola

Deje que la solicitud http ingrese a la cola, también lo llamamos procesamiento asíncrono. La idea básica es empaquetar el contexto de la solicitud recibida (es decir, solicitud y respuesta) y la lógica de procesamiento en una unidad de trabajo, y luego ponerlo en la cola, y luego la unidad de trabajo espera a que el subproceso de trabajo consumidor procese el trabajo, y luego se completa el procesamiento.devuelto al cliente. El proceso es el siguiente:02-Procesamiento de solicitud de cola.png

Habrá tres elementos clave en esta implementación: la unidad de ejecución del trabajo, la cola y el consumidor. Echemos un vistazo a sus respectivas responsabilidades e implementaciones una por una.

unidad de trabajo

La unidad de trabajo encapsula principalmente la información de contexto de la solicitud (solicitud y respuesta), la lógica de procesamiento de la solicitud y el estado de ejecución de la unidad de trabajo.

La lógica de procesamiento de la solicitud es en realidad la función específica en el proceso de procesamiento secuencial original. Si está en el modo mvc, es una acción específica en el controlador.

La forma de implementar la comunicación en Go es generalmente a través del uso de canales. Por lo tanto, hay un canal en la unidad de trabajo.Después de que la unidad de trabajo ejecuta la lógica de procesamiento específica, se escribe un mensaje en el canal para notificar a la rutina principal que la solicitud se completó y se puede devolver al cliente.

Entonces, la lógica de procesamiento para una solicitud http se ve así:

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  将w和r包装成工作单元job
  将job入队
  等待job执行完成
  本次请求处理完毕
}

Echemos un vistazo a la implementación específica de la unidad de trabajo, aquí la definimos como una estructura de trabajo:


type Job struct {
    DoneChan  chan struct{}
    handleJob func(j FlowJob) error //具体的处理逻辑
}

Job结构体中有一个handleJob,其类型是一个函数,即处理请求的逻辑部分。DoneChan通道用来让该单元进行阻塞等待,并当handleJob执行完毕后发送消息通知的。

下面我们再看看该Job的相关行为:

// 消费者从队列中取出该job时 执行具体的处理逻辑
func (job *Job) Execute() error {
    fmt.Println("job start to execute ")
    return job.handleJob(job)
}

// 执行完Execute后,调用该函数以通知主线程中等待的job
func (job *Job) Done() {
    job.DoneChan <- struct{}{}
    close(job.DoneChan)
}

// 工作单元等待自己被消费
func (job *Job) WaitDone() {
    select {
    case <-job.DoneChan:
	return
    }
}

队列

队列主要是用来存储工作单元的。是处理请求的主协程和消费协程之间的纽带。队列具有列表、容量、当前元素个数等关键元素组成。如下:

type JobQueue struct {
    mu         sync.Mutex
    noticeChan chan struct{}
    queue      *list.List
    size       int
    capacity   int
}

其行为主要有入队、出队、移除等操作。定义如下:

// 初始化队列
func NewJobQueue(cap int) *JobQueue {
    return &JobQueue{
	capacity: cap,
	queue:    list.New(),
	noticeChan: make(chan struct{}, 1),
    }
}

// 工作单元入队
func (q *JobQueue) PushJob(job *Job) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.size++
    if q.size > q.capacity {
	q.RemoveLeastJob()
    }

    q.queue.PushBack(job)


    q.noticeChan <- struct{}{}
}

// 工作单元出队
func (q *JobQueue) PopJob() *Job {
	q.mu.Lock()
	defer q.mu.Unlock()

	if q.size == 0 {
		return nil
	}

	q.size--
	return q.queue.Remove(q.queue.Front()).(*Job)
}

// 移除队列中的最后一个元素。
// 一般在容量满时,有新job加入时,会移除等待最久的一个job
func (q *JobQueue) RemoveLeastJob() {
	if q.queue.Len() != 0 {
		back := q.queue.Back()
		abandonJob := back.Value.(*Job)
		abandonJob.Done()
		q.queue.Remove(back)
	}
}

// 消费线程监听队列的该通道,查看是否有新的job需要消费
func (q *JobQueue) waitJob() <-chan struct{} {
    return q.noticeChan
}

这里我们主要解释一下入队的操作流程:

1 首先是队列的元素个数size++

2 判断size是否超过最大容量capacity

3 若超过最大容量,则将队列中最后一个元素移除。因为该元素等待时间最长,认为是超时的情况。

4 将新接收的工作单元放入到队尾。

5 往noticeChan通道中写入一个消息,以便通知消费协程处理Job。

由以上可知,noticeChan是队列和消费者协程之间的纽带。下面我们来看看消费者的实现。

消费者协程

消费者协程的职责是监听队列,并从队列中获取工作单元,执行工作单元的具体处理逻辑。在实际应用中,可以根据系统的承载能力启用多个消费协程。在本文中,为了方便讲解,我们只启用一个消费协程。

我们定义一个WorkerManager结构体,负责管理具体的消费协程。该WorkerManager有一个属性是工作队列,所有启动的消费协程都需要从该工作队列中获取工作单元。代码实现如下:


type WorkerManager struct {
    jobQueue *JobQueue
}

func NewWorkerManager(jobQueue *JobQueue) *WorkerManager {
    return &WorkerManager{
	jobQueue: jobQueue,
    }
}

func (m *WorkerManager) createWorker() error {

    go func() {
	fmt.Println("start the worker success")
	var job FlowJob

	for {
            select {
                case <-m.jobQueue.waitJob():
		fmt.Println("get a job from job queue")
                job = m.jobQueue.PopJob()
                
		fmt.Println("start to execute job")
		job.Execute()
				
                fmt.Print("execute job done")
		job.Done()
            }
	}
    }()

    return nil
}

在代码中我们可以看到,createWorker中的逻辑实际是一个for循环,然后通过select监听队列的noticeChan通道,当获取到工作单元时,就执行工作单元中的handleJob方法。执行完后,通过job.Done()方法通知在主协程中还等待的job。这样整个流程就形成了闭环。

完整代码

我们现在看下整体的处理流程,如下图: 03-Proceso general.png

现在我们写一个测试demo。在这里我们定义了一个全局的flowControl结构体,以作为队列和工作协程的管理。代码如下:

package main

import (
    "container/list"
    "fmt"
    "net/http"
    "sync"
)

func main() {
    flowControl := NewFlowControl()
    myHandler := MyHandler{
	flowControl: flowControl,
    }
    http.Handle("/", &myHandler)

    http.ListenAndServe(":8080", nil)
}

type MyHandler struct {
    flowControl *FlowControl
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Println("recieve http request")
	job := &Job{
            DoneChan: make(chan struct{}, 1),
            handleJob: func(job *Job) error {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte("Hello World"))
		return nil
            },
	}

	h.flowControl.CommitJob(job)
	fmt.Println("commit job to job queue success")
	job.WaitDone()
}

type FlowControl struct {
    jobQueue *JobQueue
    wm       *WorkerManager
}

func NewFlowControl() *FlowControl {
    jobQueue := NewJobQueue(10)
    fmt.Println("init job queue success")

    m := NewWorkerManager(jobQueue)
    m.createWorker()
    fmt.Println("init worker success")

    control := &FlowControl{
	jobQueue: jobQueue,
	wm:       m,
    }
    fmt.Println("init flowcontrol success")
    return control
}

func (c *FlowControl) CommitJob(job *Job) {
    c.jobQueue.PushJob(job)
    fmt.Println("commit job success")
}

完整的示例代码可以通过git获取:http异步处理

Un artículo anterior es la cola de prioridad, que en realidad es una versión de implementación avanzada de la cola, que puede asignar diferentes solicitudes a diferentes colas según sus prioridades. Los estudiantes interesados ​​pueden consultar: Ir al combate real | Este artículo lo llevará a comprender la implementación de la cola única a la cola de prioridad

Resumir

Encapsulando la información de contexto solicitada en una unidad de trabajo, colocándola en la cola y luego bloqueándola a través del canal de mensajes para esperar a que el consumidor complete la ejecución. Al mismo tiempo, al establecer la capacidad de la cola en la cola para resolver el problema de causar presión en el sistema debido a un exceso de solicitudes.

Supongo que te gusta

Origin juejin.im/post/7121252800469663774
Recomendado
Clasificación