本文为翻译作品,原文链接: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 方法不能改变服务器端资源的状态。总结如下:
- 所有安全的 HTTP 方法都是幂等的。
- PUT 和 DELETE 是幂等的,但因为它们可以改变服务器状态,所以是不安全的。
这使我们得出 POST 和 PATCH HTTP 方法是非幂等的。我们将在接下来的部分学习如何使这些方法幂等。
重复处理问题(Node.js API 示例)
请求可能因网络分区、磁盘故障、超时、限流或 DNS 查找问题而失败。在许多实战场景中,幂等性都是有帮助的。让我们快速浏览几个例子:
- 当你意外双击或尝试在页面处理你的请求时离开页面时,网站通常会显示一个"请等待,正在处理"弹窗。
- 多次点击电梯按钮不会改变其行为。电梯仍然会去到所需的楼层。
- 在亚马逊上将已经添加到愿望列表的物品再次加入愿望列表,只会将该物品移到愿望列表的顶部,但愿望列表中的物品仍然相同。
幂等性的应用不仅限于一个用例。这正是它作为开发人员学习的一个如此有价值的概念的原因。它与语言无关,因此没有入门障碍。
为了理解问题并找到一个可行的解决方案,我们从一个 Node.js API 示例入手👇:
javascript
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 创建一个新的短链接并将其保存到数据库。让我们运行一下:
json
{
"status": true,
"message": "短链接创建成功",
"data": {
"slug": "UfBHWg"
}
}
这是功能性的,但有两个缺陷。我们将在接下来的部分揭示它们。
在 Node.js 中为连续请求添加幂等性
我们当前的 API 实现在你每次请求相同的长 URL 时都会返回一个不同的短链接。这不仅是多余的,而且是对 CPU 和存储资源的浪费。我们需要确保在任何时候给定的长 URL 都只有一个短链接存在。
为了实现幂等性,我们可以利用数据库中保存的映射。对于给定的长 URL,我们可以检查是否存在一个短链接,并在这种情况下提前返回。
以下是生成短链接的更新代码:
javascript
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 中对并行重复请求实施幂等性
让我们并行地向我们的短链接生成器发出多个请求。以下是我们的脚本:
javascript
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",
},
};
// 其他请求处理代码
以及当我们运行上述脚本时的输出:
shell
results: ['s9SW7p', 's9SW7p', 's9SW7p', 'el42vA', 'TswIY7', 'aFwbK7', 'AYcaBM', 'gU6fP8']
我们的基础 API 在面对一些并行的重复请求时表现得不是很好。即使请求完全相同,输出也是不同的。我们需要确保并行的重复请求不会因为 Node.js 上下文切换而导致意外的副作用。
这时候就要用到锁!
在不同的进程或客户端可以访问共享资源的环境中,分布式锁至关重要。在这样的环境中,使用锁来防止竞态条件------两个或多个操作访问共享资源并尝试同时修改它,导致不可预测的结果。我们的 API 在执行并行重复请求时面临这样的挑战。通过设置一个独占锁,我们确保一次只能处理一个请求。
我们将在我们的 API 中使用 Redlock(Redis 锁)实现一个独占锁机制。Redlock 是一种分布式锁定解决方案,它有助于防止不同进程并发执行代码块。
由于竞态条件只在并行的重复请求发起时发生,我们可以在控制流中设置一个独占锁,一次只允许一个请求。重要的是要理解,两个重复的请求可以被处理,但不是在完全相同的时间。
让我们在入口点使用 Redlock 获取一个独占锁:
javascript
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(() => {});
}
}
现在,每个请求最初都尝试获取一个独占锁。如果不成功,请求被拒绝,导致锁获取错误。以下是运行相同脚本(发送并行重复请求)时的输出情况:
正如预期的那样,这 8 个并行重复请求中只有一个能生成短链接。其他请求在获取锁的阶段失败。我们终于使我们的随机短链接生成器真正地幂等了。
实战中的应用场景
我发现幂等性吸引人的地方在于其多功能性。你会注意到,无论使用哪种工具,它都被应用于不同的情况和技术中。正如我之前提到的,实际上它在以下情况中非常常见:
- 在实现一次性操作至关重要的情况下,例如在银行和股票交易所这样的交易系统中。
- 各种 GitOps 工具,如 ArgoCD,在这些工具中,重新应用相同配置不会导致冗余部署。
- 使用 React.js 工作,因为设置相同的状态不会重新渲染组件。
- 健全的财务系统,以避免双重支付问题,其中客户可能会意外被收取两次费用。
总结
在这篇文章中,我们学习了什么是幂等性,它如何防止重复处理问题,以及如何在你的 Node.js 应用程序中使用它。现在,你又多了一个工具来构建更好的 API。