改善您的API响应时间 - 将电子邮件功能放在队列上的指南
#node #redis #email #bull

介绍

开发人员总是对如何尽快做出API响应,即使使用异步功能也很好奇,一个起点将是将某些作业分开,同时仍在为用户提供快速反馈。首先要通过派对/外部服务来处理的电子邮件作业开始,API响应可能会慢慢增加,因此增加了总响应时间。
因此,与使请求同步与API的响应同步相比,我们可以利用 queues redis redis 。额外的好处是能够在每项工作失败和设置事件听众后重试工作。

先决条件

要跟随本教程,您将需要以下内容:

  • NodeJsYarn安装在您的机器上。
  • Docker安装在您的本地机器上。
  • TypescriptNodeJS的基本知识。

目录

  1. 设置Nodejs Express项目带有打字稿和Bull。
  2. 设置REDIS服务器
  3. 设置队列和流程
  4. 配置适配器以监视队列
  5. 连接到免费的redis数据库

设置Nodejs Express项目带有打字稿和Bull

我们将创建一个新项目,并在本节中安装所有依赖项。
让我们创建一个名为redis-email-tutorial的新文件夹,然后导航到在您的终端中创建的文件夹。复制,粘贴并运行以下内容。

yarn init -y && yarn add dotenv express nodemailer @bull-board/api @bull-board/express
yarn add -D typescript nodemon eslint @typescript-eslint/parser @types/nodemailer @typescript-eslint/eslint-plugin @types/node @types/express ts-node

创建一个新的 src 文件夹,然后在创建的文件夹中创建 app.ts 文件。
复制并粘贴以下;

// src/app.ts

import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";

dotenv.config();

const PORT = process.env.PORT || 3000;

const app: Express = express();

app.use(express.json());

app.post("/", async (req: Request, res: Response) => {
  res.send("Redis Email");
});

app.listen(PORT, () => {
  console.log(`\n Server is running at http://localhost:${PORT} ✅`);
});

然后编辑 package.json 文件以包含以下脚本命令

  "scripts": {
    "dev": "nodemon --quiet src/app.ts",
    "start": "node build/src/index.js",
    "build": "tsc --project ./",
    "lint": "eslint src/**/*.ts",
    "format": "eslint src/**/*.ts --fix"
  },

您现在可以通过在root目录中运行yarn dev在终端中启动该应用程序。

设置REDIS服务器

要继续此过程,请确保您在计算机上安装了Docker。您可以查看此链接Install Docker desktop有关如何进行操作。

我们现在将创建一个Docker-compose.yml文件,然后将以下内容复制到其中

# docker-compose.yml

version: "3.7"

services:
  redis:
    image: redis
    container_name: tutorial_redis_email
    environment:
      - ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 6379:6379

  redis-commander:
    container_name: email-api-redis
    image: rediscommander/redis-commander
    restart: always
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8080:8080"

让我们创建一个 .env 文件以设置PORTREDIS_URL变量

# .env

PORT=3000
REDIS_URL=http://localhost:6379

我们将确保Docker在本地机器上完全设置。然后,我们将打开终端的另一个实例,通过运行以下内容启动Docker容器;

docker-compose up

还有其他方法可以使REDIS服务器启动和运行,但是为了使Linux和Windows开发人员都保持简单,直截了当,我们将使用Docker方法。

设置队列和流程

Bull是一个节点库,它基于Redis实现了快速稳健的队列系统。因此,让我们快速浏览一些基本术语

  • 队列:队列是首先出局(FIFO)数据结构,并且队列实例通常可以具有3个主要角色:求职者,工作消费者或/和活动听众。
  • 工作生产者:求职者只是一些节点程序,可以将作业添加到队列中。
  • 工作消费者:一个工作消费者或工人,无非就是定义流程函数的节点程序。队列通过消费者流程执行每个工作。
  • 活动听众:这有助于我们聆听队列中发生的事件。活动听众都附加到每个过程。 现在让我们开始设置队列。

Folder Structure

我们将使用上面显示的文件夹结构。
从定义队列开始,让我们在src文件夹中创建一个新的文件夹queues,然后我们还将创建一个index.ts文件。将以下内容复制到创建的文件中。

// src/queues/index.ts

import Bull from "bull";
import dotenv from "dotenv";

dotenv.config();

export const emailQueue = new Bull("email", {
  redis: process.env.REDIS_URL,
});

这将为我们定义一个名为电子邮件的队列,我们​​将使用队列实例来定义生产者和消费者。
然后,我们将在队列目录中创建一个文件夹电子邮件来定义生产者和消费者。
创建两个文件 consumer.ts producer.ts 继续下面;

// src/queues/emails/producer.ts

import { emailQueue } from "..";
import { EmailData } from "./types";

export const sendMail = (data: EmailData) => {
  emailQueue.add(data, { attempts: 3 });
};

我们已经定义并导出了生产者功能以将我们的作业添加到电子邮件队列中,现在让我们定义我们的过程以执行工作。

// src/queues/emails/consumer.ts

import { Job } from "bull";
import { EmailData } from "./types";
import nodemailer from "nodemailer";

const emailProcessor = async ({ data }: Job<EmailData>) => {
  try {
    const testMailAccount = await nodemailer.createTestAccount();

    const transporter = nodemailer.createTransport({
      host: "smtp.ethereal.email",
      port: 587,
      secure: false,
      auth: {
        user: testMailAccount.user, // generated ethereal user
        pass: testMailAccount.pass, // generated ethereal password
      },
      tls: {
        rejectUnauthorized: false,
      },
    });

    const response = await transporter.sendMail(data);

    console.log("Message sent to", data?.to);

    const responseUrl = nodemailer.getTestMessageUrl(response);

    console.log(responseUrl);

    return responseUrl;
  } catch (error) {
    throw new Error(error?.toString());
  }
};

export default emailProcessor;

这定义了使用NodeMailer发送测试电子邮件到电子邮件地址的函数。

// src/queues/emails/types.ts

export type EmailData = {
  from: string;
  to: string;
  subject: string;
  html: string;
};

我们刚刚定义了我们的工作数据类型,因此让我们继续将我们的队列附加到消费者并定义某些事件听众

// src/queues/process.ts

import { Job } from "bull";
import { emailQueue } from ".";
import emailProcessor from "./emails/consumer";

const handleFailure = (job: Job, err: Error) => {
  if (job.attemptsMade >= (job?.opts?.attempts || 0)) {
    console.log(
      `Job failures above threshold in ${job.queue.name} for: ${JSON.stringify(
        job.data
      )}`,
      err
    );
    job.remove();
    return null;
  }
  console.log(
    `Job in ${job.queue.name} failed for: ${JSON.stringify(job.data)} with ${
      err.message
    }. ${(job?.opts?.attempts || 0) - job.attemptsMade} attempts left`
  );
};

const handleCompleted = (job: Job) => {
  console.log(
    `Job in ${job.queue.name} completed for: ${JSON.stringify(job.data)}`
  );
  job.remove();
};

const handleStalled = (job: Job) => {
  console.log(
    `Job in ${job.queue.name} stalled for: ${JSON.stringify(job.data)}`
  );
};

export const startQueues = () => {
  try {
    emailQueue.process(emailProcessor);
    emailQueue.on("failed", handleFailure);
    emailQueue.on("completed", handleCompleted);
    emailQueue.on("stalled", handleStalled);
    console.log("\n Queue jobs started successfully ✅");
  } catch (err) {
    console.log("Queue Jobs Error: ", err);
  }
};

我们已经将工作消费者附加到队列上,现在可以定义一个适配器,以便我们可以查看队列流程。我们将使用 Bull-board ,这是您可以用于监视队列的其他第三方UI之一。在此处查看Bull Package

// src/queues/adapter.ts
import { createBullBoard } from "@bull-board/api";
import { BullAdapter } from "@bull-board/api/bullAdapter";
import { ExpressAdapter } from "@bull-board/express";
import { emailQueue } from ".";

const serverAdapter = new ExpressAdapter();

createBullBoard({
  queues: [new BullAdapter(emailQueue)],
  serverAdapter,
});

export default serverAdapter;

最终让我们从 app.ts 文件开始我们的队列,编辑您的 app.ts.ts 文件以包含以下

// src/app.ts

import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
import { sendMail } from "./queues/emails/producer";
import { startQueues } from "./queues/process";
import serverAdapter from "./queues/adapter";

dotenv.config();

const app: Express = express();
const PORT = process.env.PORT;

app.use(express.json());

startQueues();

app.post("/mail", async (req: Request, res: Response) => {
  const { message, ...emailData } = req.body;
  await sendMail({ ...emailData, html: `<p>${message}</p>` });
  res.send("Sent Email");
});

app.use("/admin/queues", serverAdapter.getRouter());

app.listen(PORT, () => {
  console.log(`\n Server is running at http://localhost:${PORT} ✅`);
});

因此,我们导入了 Startqueues 功能以启动队列,并编辑了/mail端点以将工作对象传递到我们的电子邮件生产者中。
我们还添加了一条新的路径/admin/queues让我们检查队列。
如果您转到端点,则应该在控制台中查看您的工作数据。

连接到免费的REDIS数据库

要设置REDIS数据库进行生产,您可以检查云服务器提供的可用服务,例如Heroku RedisRedis.com等。我们将使用redis.com免费数据库用于本教程。

访问redis.com,创建一个帐户,然后继续创建一个免费的REDIS数据库。你应该有这样的东西;

redis.com Dashboard

单击connect,然后我们将复制在RedisInsight connection box中显示的URL。

redis.com dashboard

单击copy以获取完整的URL或替换您的帐户用户名和密码。它将以这种格式。

# sample link

redis://default:1dESKLjbMoOnN528sNgCiMuAPLSzO2ynJ@redis-18364.c92.us-east-1-3.ec2.cloud.redislabs.com:18364

复制链接后,我们将替换为复制链接的REDIS_URL环境变量

# .env 

PORT=3000
REDIS_URL=redis://<username>:<password>@redis-13975.c81.us-east-1-2.ec2.cloud.redislabs.com:13975

然后,您可以停止Docker实例并重新启动应用程序,任何请求发送到/mail端点仍应适用于远程Redis服务器。

结论

我们已经能够设置Redis队列来发送我们的电子邮件。队列可用于广泛的功能,您可以花时间研究这些队列以及它们如何帮助您在服务器上分开一些工作。

您可以看一下Github Repository

参考