使用 Redis 在 Node.js 中构建幂等性 API

本文为翻译作品,原文链接:Build an Idempotent API in Node.js with Redis

在微服务和分布式系统的应用里,构建稳健可靠的 API 是至关重要的。在实现可靠性方面,幂等性是一个重要课题。

本文将深入探讨幂等性的知识,探索它是什么、为什么重要,以及如何实现它来解决 API 中重复处理的持续问题。在此过程中,我们将看到如何使用 Redis 在 Node.js 中构建一个幂等性 API。

读完本文你将学会

  • 什么是幂等性?
  • 为什么幂等性重要
  • 幂等的 HTTP 方法
  • 在 Node.js 中为连续请求添加幂等性
  • 在 Node.js 中对并行重复请求实施幂等性
  • 幂等性实战应用场景

你可以在 GitHub 上看到本文中使用的所有代码。

什么是幂等性?

如果一个操作多次应用具有与一次应用相同的效果,则该操作被视为幂等的。在 REST API 中,这意味着执行多个 HTTP 请求应该与执行单个 HTTP 请求有相同的结果(不是输出)。

为什么幂等性重要

幂等性是构建可靠和可扩展 API 的重要方案。虽然对于个人项目来说,拥有一个非幂等的 API 可能是可以接受的,但在分布式系统的背景下,这是一个重点需求。

在典型的分布式系统中,各种服务相互作用。要完成一个操作,请求必须通过多个阶段,可能会遇到网络问题、磁盘故障、扩展延迟,甚至偶尔服务离线。在这样复杂的情况下,请求在某个点上需要被重试几乎是肯定的。

在以上场景中,为了让重试成功,所有操作都必须遵循幂等性原则。重试请求不应产生错误或非预期的结果。

幂等的 HTTP 方法

一些 HTTP 方法默认是幂等的。例如,使用 GET 方法获取资源的详情是幂等的,因为它总是为给定输入提供相同的结果。同样,以下方法也是幂等的:

  • HEAD
  • OPTIONS
  • TRACE
  • PUT
  • DELETE

注意,最后两种方法(PUT 和 DELETE)是幂等的,但不安全。一个安全的 HTTP 方法不能改变服务器端资源的状态。总结如下:

  1. 所有安全的 HTTP 方法都是幂等的。
  2. PUT 和 DELETE 是幂等的,但因为它们可以改变服务器状态,所以是不安全的。

这使我们得出 POST 和 PATCH HTTP 方法是非幂等的。我们将在接下来的部分学习如何使这些方法幂等。

重复处理问题(Node.js API 示例)

请求可能因网络分区、磁盘故障、超时、限流或 DNS 查找问题而失败。在许多实战场景中,幂等性都是有帮助的。让我们快速浏览几个例子:

  • 当你意外双击或尝试在页面处理你的请求时离开页面时,网站通常会显示一个“请等待,正在处理”弹窗。
  • 多次点击电梯按钮不会改变其行为。电梯仍然会去到所需的楼层。
  • 在亚马逊上将已经添加到愿望列表的物品再次加入愿望列表,只会将该物品移到愿望列表的顶部,但愿望列表中的物品仍然相同。

幂等性的应用不仅限于一个用例。这正是它作为开发人员学习的一个如此有价值的概念的原因。它与语言无关,因此没有入门障碍。

为了理解问题并找到一个可行的解决方案,我们从一个 Node.js API 示例入手👇:

async function create(req, res) {
  try {
    const { longURL } = req.body;
 
    // 新的长 URL,生成一个新的且唯一的短链接
    const slug = await urlService.generateNewSlug();
 
    // 将短链接 <> 长 URL 映射保存到数据库
    await urlService.saveToDB(slug, longURL);
 
    // 返回新生成的短链接
    return res.status(HTTP_STATUS_CODES.SUCCESS).json({
      status: true,
      message: "短链接创建成功",
      data: { slug },
    });
  } catch (err) {
    return res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR).json({
      status: false,
      message: "无法创建短链接",
      error: err,
    });
  }
}

它为传递给它的每个长 URL 创建一个新的短链接并将其保存到数据库。让我们运行一下:

{
  "status": true,
  "message": "短链接创建成功",
  "data": {
    "slug": "UfBHWg"
  }
}

这是功能性的,但有两个缺陷。我们将在接下来的部分揭示它们。

在 Node.js 中为连续请求添加幂等性

我们当前的 API 实现在你每次请求相同的长 URL 时都会返回一个不同的短链接。这不仅是多余的,而且是对 CPU 和存储资源的浪费。我们需要确保在任何时候给定的长 URL 都只有一个短链接存在。

为了实现幂等性,我们可以利用数据库中保存的映射。对于给定的长 URL,我们可以检查是否存在一个短链接,并在这种情况下提前返回。

以下是生成短链接的更新代码:

async function create(req, res) {
  try {
    const { longURL } = req.body;
 
    // 检查映射是否已经存在
    const mapping = await service.urlService.findByLongURL(longURL);
    if (mapping) {
      return res.status(HTTP_STATUS_CODES.SUCCESS).json({
        status: true,
        message: "短链接创建成功",
        data: { slug: mapping.slug },
      });
    }
 
    // 新的长 URL,生成一个新的且唯一的短链接
    const slug = await urlService.generateNewSlug();
 
    // 将短链接 <> 长 URL 映射保存到数据库
    await urlService.saveToDB(slug, longURL);
 
    // 返回新生成的短链接
    return res.status(HTTP_STATUS_CODES.SUCCESS).json({
      status: true,
      message: "短链接创建成功",
      data: { slug },
    });
  } catch (err) {
    return res.status(HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR).json({
      status: false,
      message: "无法创建短链接",
      error: err,
    });
  }
}

请注意,我们现在在生成新短链接之前检查我们的数据库中是否有映射。如果映射存在,我们返回存储的短链接。否则,我们生成一个新的(并将其存储在数据库中)。

这确保我们只处理一次长 URL。

问题解决了吗?

幂等性并不要求每个重复请求都有相同的 API 响应。例如,在使用创建 API 时,初始请求可能返回一个 200 状态码,而后续的重复请求可能返回一个 202 状态码,并且不再次创建资源。尽管响应不同,API 的行为仍然是幂等的。

在 Node.js 中对并行重复请求实施幂等性

让我们并行地向我们的短链接生成器发出多个请求。以下是我们的脚本:

const async = require("async");
const axios = require("axios");
 
const payload = {
  method: "post",
  url: `http://localhost:8201/url/create`,
  headers: { "Content-Type": "application/json" },
  data: {
    longURL: "http://example.com/100",
  },
};
 
// 其他请求处理代码

以及当我们运行上述脚本时的输出:

results:  ['s9SW7p', 's9SW7p', 's9SW7p', 'el42vA', 'TswIY7', 'aFwbK7', 'AYcaBM', 'gU6fP8']

我们的基础 API 在面对一些并行的重复请求时表现得不是很好。即使请求完全相同,输出也是不同的。我们需要确保并行的重复请求不会因为 Node.js 上下文切换而导致意外的副作用。

这时候就要用到锁!

在不同的进程或客户端可以访问共享资源的环境中,分布式锁至关重要。在这样的环境中,使用锁来防止竞态条件——两个或多个操作访问共享资源并尝试同时修改它,导致不可预测的结果。我们的 API 在执行并行重复请求时面临这样的挑战。通过设置一个独占锁,我们确保一次只能处理一个请求。

我们将在我们的 API 中使用 Redlock(Redis 锁)实现一个独占锁机制。Redlock 是一种分布式锁定解决方案,它有助于防止不同进程并发执行代码块。

由于竞态条件只在并行的重复请求发起时发生,我们可以在控制流中设置一个独占锁,一次只允许一个请求。重要的是要理解,两个重复的请求可以被处理,但不是在完全相同的时间。

让我们在入口点使用 Redlock 获取一个独占锁:

async function create(req, res) {
  let lock;
  try {
    // 尝试获取锁
    lock = await service.urlService.acquireLock(
      "URL:CREATE:ExclusiveLock",
      100
    );
 
    // 检查映射是否已经存在
    // 生成新的短链接并保存到数据库的逻辑
  } catch (err) {
    // 错误处理
  } finally {
    // 最后释放锁
    if (lock) await lock.release().catch(() => {});
  }
}

现在,每个请求最初都尝试获取一个独占锁。如果不成功,请求被拒绝,导致锁获取错误。以下是运行相同脚本(发送并行重复请求)时的输出情况:

使用 Redis 在 Node.js 中构建幂等性 API

正如预期的那样,这 8 个并行重复请求中只有一个能生成短链接。其他请求在获取锁的阶段失败。我们终于使我们的随机短链接生成器真正地幂等了。

实战中的应用场景

我发现幂等性吸引人的地方在于其多功能性。你会注意到,无论使用哪种工具,它都被应用于不同的情况和技术中。正如我之前提到的,实际上它在以下情况中非常常见:

  • 在实现一次性操作至关重要的情况下,例如在银行和股票交易所这样的交易系统中。
  • 各种 GitOps 工具,如 ArgoCD,在这些工具中,重新应用相同配置不会导致冗余部署。
  • 使用 React.js 工作,因为设置相同的状态不会重新渲染组件。
  • 健全的财务系统,以避免双重支付问题,其中客户可能会意外被收取两次费用。

总结

在这篇文章中,我们学习了什么是幂等性,它如何防止重复处理问题,以及如何在你的 Node.js 应用程序中使用它。现在,你又多了一个工具来构建更好的 API。

原文链接:https://juejin.cn/post/7334523075467378728 作者:前端为什么

(0)
上一篇 2024年2月16日 上午10:05
下一篇 2024年2月16日 上午10:17

相关推荐

发表回复

登录后才能评论