作者选择了/dev/color作为Write for DOnations计划的捐赠对象。
介绍
大多数应用程序都依赖于数据,无论数据来自数据库还是 API。从 API 获取数据会向 API 服务器发送网络请求,并将数据作为响应返回。这些往返旅程需要时间,可能会增加应用程序对用户的响应时间。此外,大多数 API 限制它们在特定时间范围内为应用程序提供服务的请求数量,这个过程被称为速率限制。
为了解决这些问题,您可以缓存您的数据,使应用程序只向 API 发送一次请求,所有后续的数据请求将从缓存中检索数据。Redis是一种将数据存储在服务器内存中的流行工具,用于缓存数据。您可以使用node-redis
模块在 Node.js 中连接到 Redis,该模块为您提供了在 Redis 中检索和存储数据的方法。
在本教程中,您将构建一个使用 Express 应用程序,利用 axios
模块从 RESTful API 检索数据。接下来,您将修改应用程序,使用 node-redis
模块将从 API 检索的数据存储在 Redis 中。之后,您将实现缓存有效期,使缓存在经过一定时间后过期。最后,您将使用 Express 中间件来缓存数据。
先决条件
要按照本教程,您需要:
-
在您的服务器上设置 Node.js 环境。如果您使用的是 Ubuntu 22.04,则通过按照 在 Ubuntu 22.04 上安装 Node.js 的第 3 选项 进行安装最新版本的 Node.js 和 npm。对于其他操作系统,请参阅 如何安装 Node.js 并创建本地开发环境 系列。
-
在您的服务器上安装了Redis。如果您使用的是Ubuntu 22.04,请按照在Ubuntu 22.04上安装和保护Redis的步骤1和2。如果您使用的是其他操作系统,请参阅如何安装和保护Redis。
-
异步编程的知识。请参考理解JavaScript中的事件循环、回调、Promises和Async/Await。
-
使用Express Web框架的基本知识。请参阅如何开始使用Node.js和Express。
第1步 —— 设置项目
在这一步中,您将安装此项目所需的依赖项,并启动一个Express服务器。在本教程中,您将创建一个包含有关不同种类鱼的信息的维基。我们将项目命名为fish_wiki
。
首先,使用mkdir
命令创建项目目录:
进入该目录:
使用npm
命令初始化package.json
文件:
-y
选项会自动接受所有默认值。
运行npm init
命令时,将在您的目录中创建package.json
文件,内容如下:
OutputWrote to /home/your_username/<^>fish_wiki<^/package.json:
{
"name": "fish_wiki",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
接下来,您将安装以下软件包:
express
:用于Node.js的Web服务器框架。axios
:Node.js的HTTP客户端,用于进行API调用。node-redis
:允许您在Redis中存储和访问数据的Redis客户端。
要一次安装这三个软件包,请输入以下命令:
安装软件包后,您将创建一个基本的Express服务器。
使用nano
或您选择的文本编辑器,创建并打开server.js
文件:
在server.js
文件中,输入以下代码以创建一个Express服务器:
首先,将express
导入文件。 在第二行,将app
变量设置为express
的实例,这样您就可以访问诸如get
、post
、listen
等方法。本教程将重点介绍get
和listen
方法。
在接下来的一行中,将port
变量定义并赋值为您希望服务器监听的端口号。 如果环境变量文件中没有可用的端口号,则将使用端口3000
作为默认值。
最后,使用 app
变量,调用 express
模块的 listen()
方法来在端口 3000
上启动服务器。
保存并关闭文件。
使用 node
命令运行 server.js
文件以启动服务器:
控制台将记录类似以下的消息:
OutputApp listening on port 3000
输出确认服务器正在运行并准备好在端口 3000
上提供任何请求。由于 Node.js 不会在文件更改时自动重新加载服务器,您现在将使用 CTRL+C
停止服务器,以便在下一步中更新 server.js
。
安装了依赖项并创建了 Express 服务器后,您将从 RESTful API 中检索数据。
步骤 2 —— 检索不使用缓存的 RESTful API 中的数据
在此步骤中,您将在之前步骤中构建的 Express 服务器基础上检索不使用缓存的 RESTful API 中的数据,演示未存储数据时会发生什么。
首先,在您的文本编辑器中打开 server.js
文件:
接下来,您将从 FishWatch API 中检索数据。FishWatch API 返回有关鱼类物种的信息。
在你的server.js
文件中,使用以下突出显示的代码定义一个请求API数据的函数:
在第二行,你导入了axios
模块。接下来,你定义了一个异步函数fetchApiData()
,它以species
作为参数。为了使函数成为异步函数,你在其前面加上了async
关键字。
在函数内部,你使用axios
模块的get()
方法,并传入你想要从中检索数据的API端点,例如在这个例子中是FishWatch API。由于get()
方法实现了一个promise,你在其前面加上await
关键字来解析这个promise。一旦promise被解析并从API返回数据,你调用console.log()
方法。console.log()
方法将记录一条消息,表示已向API发送了一个请求。最后,你返回来自API的数据。
接下来,你将定义一个接受GET
请求的Express路由。在你的server.js
文件中,使用以下代码定义路由:
在上述代码块中,你调用了express
模块的get()
方法,该方法仅监听GET
请求。该方法接受两个参数:
/fish/:species
:Express将监听的端点。该端点接受一个路由参数:species
,用于捕获URL中该位置输入的任何内容。getSpeciesData()
(尚未定义):当URL匹配第一个参数指定的端点时将调用的回调函数。
现在路由已经定义,指定getSpeciesData
回调函数:
getSpeciesData
函数是一个异步处理函数,作为第二个参数传递给express
模块的get()
方法。 getSpeciesData()
函数接受两个参数:一个请求对象和一个响应对象。请求对象包含有关客户端的信息,而响应对象包含将从Express发送到客户端的信息。
接下来,将突出显示的代码添加到getSpeciesData()
回调函数中调用fetchApiData()
以从API中检索数据:
在该函数中,您从req.params
对象中提取从端点捕获的值,然后将其分配给species
变量。在下一行中,您定义变量results
并将其设置为undefined
。
之后,您使用species
变量作为参数调用fetchApiData()
函数。fetchApiData()
函数调用前缀为await
语法,因为它返回一个promise。当promise解析时,它返回数据,然后被赋值给results
变量。
接下来,添加以下突出显示的代码来处理运行时错误:
您定义了try/catch
块来处理运行时错误。在try
块中,您调用fetchApiData()
来从API获取数据。如果遇到错误,catch
块会记录错误并返回404
状态码以及“数据不可用”的响应。
大多数API在没有特定查询数据时返回404状态码,这会自动触发catch
块执行。但是,FishWatch API在没有特定查询数据时返回200状态码和一个空数组。200状态码表示请求成功,因此catch()
块永远不会被触发。
为了触发catch()
块,您需要检查数组是否为空,并在if
条件评估为true时抛出错误。当if
条件评估为false时,您可以向客户端发送包含数据的响应。
为此,请添加以下突出显示的代码:
一旦数据从API返回,if
语句将检查results
变量是否为空。如果条件满足,您将使用throw
语句抛出自定义错误,其中包含消息API返回了空数组
。执行完成后,执行流将切换到catch
块,该块记录错误消息并返回404响应。
相反,如果results
变量包含数据,则if
语句的条件将不会满足。因此,程序将跳过if
块并执行响应对象的send
方法,该方法将响应发送到客户端。
send
方法接受一个具有以下属性的对象:
-
fromCache
: 此属性接受一个值,帮助您了解数据是来自Redis缓存还是API。您现在分配了一个false
值,因为数据来自API。 -
data
: 此属性分配了包含从API返回的数据的results
变量。
此时,您的完整代码如下:
现在一切就绪,保存并退出您的文件。
启动express服务器:
Fishwatch API接受许多物种,但在本教程中,我们将仅使用red-snapper
鱼种作为您将在整个教程中测试的路由参数。
现在在您的本地计算机上启动您喜欢的网络浏览器。导航至http://localhost:3000/fish/red-snapper
URL。
注意:如果您正在远程服务器上进行教程,请使用端口转发在浏览器中查看应用程序。
保持Node.js服务器仍在运行的情况下,在本地计算机上打开另一个终端,然后输入以下命令:
连接到服务器后,在本地计算机的Web浏览器中导航至http://localhost:3000/fish/red-snapper
。
页面加载完成后,您应该会看到fromCache
设置为false
。
现在,刷新URL三次并查看您的终端。终端将记录“Request sent to the API”,次数与您刷新浏览器的次数相同。
如果您在初始访问后刷新了URL三次,则您的输出将如下所示:
Output
App listening on port 3000
Request sent to the API
Request sent to the API
Request sent to the API
Request sent to the API
此输出显示每次刷新浏览器时都会向API服务器发送网络请求。如果您的应用程序有1000个用户访问相同的端点,那么就会向API发送1000个网络请求。
当您实施缓存时,对API的请求将仅执行一次。所有后续请求将从缓存中获取数据,提高您的应用程序性能。
现在,停止您的Express服务器,使用CTRL+C
。
现在您可以从 API 请求数据并将其提供给用户,您将在 Redis 中缓存从 API 返回的数据。
第三步 — 使用 Redis 缓存 RESTful API 请求
在本节中,您将缓存来自 API 的数据,以便只有对您的应用端点的初始访问将从 API 服务器请求数据,随后的所有请求将从 Redis 缓存中获取数据。
打开 server.js
文件:
在您的 server.js
文件中,导入 node-redis
模块:
在同一文件中,通过添加下面的高亮代码连接到 Redis,使用 node-redis
模块:
首先,您将 redisClient
变量定义为 undefined。然后,您定义了一个匿名的自调用异步函数,这是一个在定义后立即运行的函数。您通过将一个无名称的函数定义封装在括号中 (async () => {...})
来定义一个匿名的自调用异步函数。为了使其自调用,您立即在其后跟另一组括号 ()
,看起来像 (async () => {...})()
。
在函数内部,你调用了 redis
模块的 createClient()
方法来创建一个 redis
对象。由于你在调用 createClient()
方法时没有提供 Redis 使用的端口,Redis 将使用默认端口 6379
。
你还调用了 Node.js 的 on()
方法来在 Redis 对象上注册事件。on()
方法接受两个参数: error
和回调函数。第一个参数 error
是在 Redis 遇到错误时触发的事件。第二个参数是一个在 error
事件被触发时运行的回调函数。回调函数会将错误记录在控制台中。
最后,你调用了 connect()
方法,它会在默认端口 6379
上启动与 Redis 的连接。connect()
方法返回一个 Promise,所以你要使用 await
语法来解决它。
现在你的应用已经连接到了 Redis,你将修改 getSpeciesData()
回调函数,在初始访问时将数据存储在 Redis 中,并在后续的所有请求中从缓存中检索数据。
在你的 server.js
文件中,添加并更新高亮显示的代码:
在getSpeciesData
函数中,您将isCached
变量定义为false
。在try
块中,您调用node-redis
模块的get()
方法,并将species
作为参数传递。当该方法在Redis中找到与species
变量值匹配的键时,它将返回数据,然后将其分配给cacheResults
变量。
接下来,一个if
语句检查cacheResults
变量是否有数据。如果条件满足,则将isCache
变量赋值为true
。随后,您调用JSON
对象的parse()
方法,并将cacheResults
作为参数传递。parse()
方法将JSON字符串数据转换为JavaScript对象。在解析JSON后,您调用send()
方法,该方法接受一个具有fromCache
属性设置为isCached
变量的对象。该方法将响应发送给客户端。
如果node-redis
模块的get()
方法在缓存中未找到数据,则cacheResults
变量设置为null
。因此,if
语句的计算结果为false。当发生这种情况时,执行将跳过到else
块,您将调用fetchApiData()
函数从API获取数据。然而,一旦从API返回数据,它就不会保存在Redis中。
要将数据存储在Redis缓存中,您需要使用node-redis
模块的set()
方法将其保存。为此,请添加下面突出显示的行:
在else
块中,一旦数据被获取,你调用node-redis
模块的set()
方法将数据保存到Redis中,键名为species
变量的值。
set()
方法接受两个参数,即键值对:species
和JSON.stringify(results)
。
第一个参数species
是数据将在Redis中保存的键名。记住species
被设置为你定义的端点传递的值。例如,当你访问/fish/red-snapper
时,species
被设置为red-snapper
,这将成为Redis中的键。
第二个参数JSON.stringify(results)
是键的值。在第二个参数中,你使用results
变量作为参数调用了JSON
的stringify()
方法,该方法包含从API返回的数据。该方法将JSON转换为字符串;这就是为什么当你使用node-redis
模块的get()
方法从缓存中检索数据时,你之前使用JSON.parse
方法调用cacheResults
变量作为参数的原因。
你的完整文件现在将如下所示:
保存并退出你的文件,然后使用node
命令运行server.js
:
一旦服务器启动,刷新浏览器中的http://localhost:3000/fish/red-snapper
。
注意fromCache
仍然被设置为false
:
现在再次刷新页面,查看这次fromCache
是否设置为true
:
刷新页面五次,然后返回终端。您的输出将类似于以下内容:
OutputApp listening on port 3000
Request sent to the API
现在,已发送到API的请求
仅在多次URL刷新后被记录了一次,与上一节在每次刷新时记录消息形成对比。此输出确认只发送了一个请求到服务器,随后从Redis获取数据。
为了进一步确认数据已存储在Redis中,请使用CTRL+C
停止您的服务器。使用以下命令连接到Redis服务器客户端:
检索键red-snapper
下的数据:
您的输出将类似于以下内容(经过简化编辑):
Output"[{\"Fishery Management\":\"<ul>\\n<li><a...3\"}]"
输出显示了当您访问/fish/red-snapper
端点时API返回的JSON数据的字符串版本,这证实了API数据已存储在Redis缓存中。
退出Redis服务器客户端:
现在您可以缓存来自API的数据,还可以设置缓存的有效期。
步骤 4 —— 实现缓存有效期
在缓存数据时,您需要知道数据变化的频率。一些API数据每隔几分钟就会更改;而其他数据可能是每隔几小时、几周、几个月或几年。设置合适的缓存持续时间可以确保您的应用向用户提供最新的数据。
在这一步中,您将为需要存储在Redis中的API数据设置缓存有效性。当缓存过期时,您的应用程序将发送请求到API以检索最新数据。
您需要查阅API文档以设置正确的缓存到期时间。大多数文档都会提到数据更新的频率。但是,有些情况下文档可能没有提供这些信息,因此您可能需要猜测。检查各种API端点的last_updated
属性可以显示数据更新的频率。
选择缓存持续时间后,您需要将其转换为秒数。在本教程中,为了演示方便,您将设置缓存持续时间为3分钟或180秒。这个示例持续时间将使得测试缓存持续时间功能更加容易。
要实现缓存有效期持续时间,请打开server.js
文件:
添加以下突出显示的代码:
在node-redis
模块的set()
方法中,您传递一个具有以下属性的对象作为第三个参数:
EX
:接受以秒为单位的缓存持续时间的值。NX
:当设置为true
时,确保set()
方法仅在Redis中不存在该键时才设置键。
保存并退出您的文件。
回到 Redis 服务器客户端以测试缓存的有效性:
删除 Redis 中的 red-snapper
键:
退出 Redis 客户端:
现在,使用 node
命令启动开发服务器:
切换回浏览器,刷新 http://localhost:3000/fish/red-snapper
URL。在接下来的三分钟内,如果你刷新该 URL,则终端输出应与以下输出一致:
OutputApp listening on port 3000
Request sent to the API
三分钟过后,在浏览器中刷新 URL。在终端中,你应该看到“Request sent to the API”被记录了两次。
OutputApp listening on port 3000
Request sent to the API
Request sent to the API
这个输出显示了缓存已过期,并且再次向 API 发送了请求。
你可以停止 Express 服务器。
现在,你可以设置缓存的有效期,接下来将使用中间件缓存数据。
第 5 步 —— 在中间件中缓存数据
在这一步中,你将使用 Express 中间件来缓存数据。中间件是一个函数,它可以访问请求对象、响应对象,并在执行后运行的回调函数。在使用中间件时,你可以修改请求和响应对象,或更早地向用户返回响应。
要在应用程序中使用中间件进行缓存,您需要修改getSpeciesData()
处理程序函数,以从 API 获取数据并将其存储在 Redis 中。您将所有查找 Redis 中数据的代码移到cacheData
中间件函数中。
当您访问/fish/:species
端点时,中间件函数将首先运行以在缓存中搜索数据;如果找到,它将返回响应,并且getSpeciesData
函数将不会运行。然而,如果中间件在缓存中找不到数据,它将调用getSpeciesData
函数从 API 获取数据并将其存储在 Redis 中。
首先,打开您的server.js
:
接下来,删除突出显示的代码:
在getSpeciesData()
函数中,您删除所有查找 Redis 中存储数据的代码。您还删除isCached
变量,因为getSpeciesData()
函数将仅从 API 获取数据并将其存储在 Redis 中。
一旦代码被移除,将fromCache
设置为false
,如下所示突出显示,使得getSpeciesData()
函数看起来像下面这样:
getSpeciesData()
函数从 API 检索数据,将其存储在缓存中,并向用户返回响应。
接下来,添加以下代码来定义在 Redis 中缓存数据的中间件函数:
cacheData()
中间件函数接受三个参数:req
、res
和 next
。在 try
块中,该函数检查 species
变量中的值是否在 Redis 中以其键存储了数据。如果数据在 Redis 中,则将其返回并设置到 cacheResults
变量中。
接下来,if
语句检查 cacheResults
是否有数据。如果评估为 true,则将数据保存在 results
变量中。然后,中间件使用 send()
方法返回一个具有属性 fromCache
设置为 true
和 data
设置为 results
变量的对象。
然而,如果 if
语句评估为 false,则执行切换到 else
块。在 else
块内,调用 next()
,将控制传递给应在其后执行的下一个函数。
为了在调用 next()
时使 cacheData()
中间件将控制传递给 getSpeciesData()
函数,请相应地更新 express
模块的 get()
方法:
现在,get()
方法将 cacheData
作为其第二个参数,这是一个中间件,用于查找 Redis 中缓存的数据,并在找到时返回响应。
现在,当您访问/fish/:species
端点时,cacheData()
首先执行。如果数据已缓存,则返回响应,并且请求-响应周期在此结束。但是,如果在缓存中找不到数据,则将调用getSpeciesData()
来从API检索数据,将其存储在缓存中,并返回响应。
完整的文件现在看起来像这样:
保存并退出您的文件。
要正确测试缓存,您可以在Redis中删除red-snapper
键。要执行此操作,请进入Redis客户端:
删除red-snapper
键:
退出Redis客户端:
现在,运行server.js
文件:
一旦服务器启动,返回浏览器并再次访问http://localhost:3000/fish/red-snapper
。多次刷新它。
终端将记录发送到API的请求消息。 cacheData()
中间件将为接下来的三分钟内的所有请求提供服务。如果您在四分钟的时间段内随机刷新URL,则您的输出将类似于这样:
OutputApp listening on port 3000
Request sent to the API
Request sent to the API
该行为与上一节中应用程序的工作方式一致。
现在,您可以使用中间件在Redis中缓存数据。
结论
在本文中,您构建了一个应用程序,该应用程序从API获取数据,并将数据作为响应返回给客户端。然后,您修改了应用程序,将API响应缓存到Redis中,并为所有后续请求提供来自缓存的数据。您修改了缓存持续时间,以在一定时间后过期,并使用中间件来处理缓存数据检索。
作为下一步,您可以探索Node Redis文档,以了解node-redis
模块中提供的更多功能。您还可以阅读Axios和Express文档,深入了解本教程涵盖的主题。
要继续构建您的Node.js技能,请参阅如何在Node.js中编码系列。
Source:
https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis