读书笔记《building-restful-web-services-with-spring-5-second-edition》性能
当涉及到应用程序中的 RESTful Web 服务时,性能被认为是主要标准。本章将主要关注如何提高应用程序的性能并减少响应时间。尽管性能优化技术可以应用于 Web 应用程序的不同层,但我们将讨论 RESTful(Web)层。其余的性能优化技术将在第 11 章,缩放中讨论。
本章将讨论以下主题:
- HTTP compression
- HTTP caching and HTTP cache control
- Cache implementation in the REST API
- Using HTTP If-Modified-Since headers and ETags
为了从 REST 服务快速获取内容,可以通过 HTTP 等协议压缩和发送数据。在压缩数据时,我们必须遵循一些编码格式,因此接收端将应用相同的格式。
当请求服务器中的资源时,客户端将有许多选项来接收各种表示形式的内容。例如,DOC/PDF 是数据类型表示。土耳其语或英语是语言表示,服务器可以在其中以特定语言发送资源。服务端和客户端之间必须就资源将被访问的格式达成某种协议,例如语言、数据类型等。该过程称为内容协商。
在这里,我们将讨论两种不同的内容协商机制:服务器驱动和代理驱动机制。在继续讨论这些机制之前,我们将讨论 Accept-Encoding 和 Content-Encoding,因为它们很重要。
客户端将告诉服务器它可以接收哪些 compression 算法。最常见的编码类型是 gzip
和 deflate
。在请求服务器时,客户端将在请求标头中共享编码类型。接受编码将用于此类目的。简单地说,客户端会对服务器说“我只接受提到的压缩格式”。
我们将看到示例 Accept-Encoding
如下:
在前面的标头中,客户端说它在响应中只能接受 gzip
或 deflate
。
其他可能的选项如下所述:
我们可以看到 compress
值后跟 q=0.5
,这意味着质量等级只有 0.5
与 q=1.0
的 gzip
评级相比,这是非常高的。在这种情况下,客户端建议服务器可以通过 compress
获取 gzip
。但是,如果 gzip
是不可能的,则 compress
对客户端来说很好。
如果服务器不支持客户端请求的压缩算法,则服务器应发送带有 406 (Not Acceptable)
状态码的错误响应。
Content-Encoding 是一个实体头,使用 压缩要从服务器发送到客户端的数据类型。 Content-Encoding 值告诉客户端在实体主体中使用了哪些编码。它将告诉客户端如何解码数据以检索值。
让我们看一下单个和多个编码选项:
在上述配置中,Content-Encoding 提供了单选项和多选项。在这里,服务器告诉客户端它可以提供基于 gzip
和 compress
算法的编码。如果服务器提到了多重编码,这些编码将按照提到的顺序应用。
当客户端多次请求相同的 resource 表示时,从服务器端提供它会浪费时间这在 Web 应用程序中会很耗时。如果资源被重用,而不是与服务器对话,它肯定会提高 Web 应用程序的性能。
缓存将被视为为我们的 Web 应用程序带来性能的主要选项。 Web缓存避免服务器多次接触,减少延迟;因此,应用程序会更快。缓存可以应用于应用程序的不同层。在本章中,我们将只讨论 HTTP 缓存,它被认为是一个中间层。我们将在 第 11 章,缩放中深入研究其他形式的缓存>。
缓存控件是一个标头field,它指定了Web上缓存操作的指令。这些指令给予缓存授权,定义缓存的持续时间,等等。指令定义行为,通常旨在防止缓存响应。
在这里,我们将讨论 HTTP 缓存指令:public
、private
、no-cache
和 only-if-cached
指令。
如果缓存控件允许公共缓存,则 resource 可以被多个用户缓存缓存。我们可以通过在 Cache-Control
标头中设置 public
选项来做到这一点。在公共缓存中,响应可能会被多个用户缓存,即使是不可缓存或可缓存的,也只能在非共享缓存中进行缓存:
上述设置中,public
表示响应可以被任意缓存缓存。
与公共缓存不同,私有 responses 适用于单个用户缓存,而不适用于共享缓存。在私有缓存中,中间体不能缓存内容:
前面的设置表明响应只对单个用户可用,不应被任何其他缓存访问。
此外,我们可以在标题设置中指定内容应缓存多长时间。这可以通过 max-age
指令选项来完成。
检查以下设置:
在前面的设置中,我们提到了可以在私有模式下缓存响应(仅限单用户)以及资源被认为是新鲜的最长时间。
访问动态资源可能不需要缓存。在这种情况下,我们可以在缓存控件中使用 no-cache
设置来避免客户端缓存:
The preceding setting will tell the client to check the server whenever the resource is being requested.
此外,在某些情况下,我们可能需要禁用缓存机制本身。这可以在我们的设置中使用 no-store
来完成:
上述设置将告诉客户端避免资源缓存并始终从服务器获取资源。
当缓存有一个新的 entry 可以在客户端请求时用作响应时,它将与原始服务器进行检查查看缓存的条目是否仍然可用。此过程称为缓存验证。此外,当用户按下重新加载按钮时,会触发重新验证。如果缓存响应中包含Cache-Control: must revalidate
头,正常浏览下会触发。
当资源的时间到期时,它将被 validated 或再次获取。仅当服务器提供强或弱验证器时才会触发缓存验证。
ETags 提供了一个机制 来验证缓存的响应。 ETag 响应标头可用作强验证器。在这种情况下,客户既无法理解价值,也无法预测其价值。当服务器发出响应时,它会生成一个隐藏资源状态的令牌:
如果 ETag
是响应的一部分,则客户端可以在未来请求的标头中发出 If-None-Match
验证缓存的资源:
服务器会将请求的标头与资源的当前状态进行比较。如果资源状态发生变化,服务器将响应一个新资源。否则,服务器将返回 304 Not Modified
响应。
到目前为止,我们已经看到了一个强大的验证器(ETags)。在这里,我们将讨论一个 可以 在标头中使用的弱验证器。 Last-Modified
响应标头可用作弱验证器。不是生成资源的哈希,而是使用时间戳来检查缓存的响应是否有效。
由于此验证器的分辨率为 1 秒,因此与 ETags 相比,它被认为是弱的。如果响应中存在 Last-Modified
标头,则客户端可以发送 If-Modified-Since
请求标头到验证缓存的资源。
If-Modified-Since
标头在请求资源时由客户端提供。为了简化实际示例中的机制,客户端请求将类似于:“我已经在上午 10 点缓存了资源 XYZ;但是,如果它从上午 10 点开始更改,则获取更新的 XYZ。否则只返回 304
。那我就用之前缓存的XYZ。”
到目前为止,我们已经在 this 章节中看到了理论部分。让我们尝试在我们的应用程序中实现这个概念。为了简化缓存实现,我们将只使用用户管理。我们将使用 getUser
(单用户)REST API 来应用我们的缓存概念。
在 getUser
方法中,我们将正确的 userid
传递给路径变量,假设客户端将传递 userid
并获取资源。有许多缓存选项可供实现。在这里,我们将只使用 If-Modified-Since
缓存机制。由于这个机制会在header中传递If-Modified-Since
的值,然后转发给服务器,表示如果资源在指定时间后发生变化,则获取资源新鲜,否则返回 null。
我们可以通过多种方式实现缓存。由于我们的目标是简化和清楚地传达信息,我们将保持代码简单,而不是增加代码的复杂性。为了实现 this 缓存,我们可能需要添加一个名为 updatedDate< /code> 在我们的
User
类中。让我们在我们的类中添加变量。
updatedDate
变量将用作 If-Modified-Since
缓存的检查变量,因为我们将依赖 user-updated日期。
客户端将询问服务器自上次缓存时间以来用户数据是否已更改。服务器将检查用户updatedDate
,如果没有更新则返回null;否则,否则它将返回新数据:
在前面的代码中,我们刚刚添加了一个新变量 updatedDate
,并在其中添加了正确的 getter 和 setter 方法。稍后我们可能会通过添加 Lombok 库来清理这些 getter 和 setter 方法。我们将在接下来的章节中应用 Lombok。
另外,当我们获得类的实例时,我们需要添加另一个构造函数来初始化 updatedDate
变量。让我们在这里添加构造函数:
如果可能,我们可以将 toString
方法更改如下:
添加前面提到的所有细节后,我们的类将如下所示:
现在,我们将回到我们在前面章节中介绍的 UserController
,并更改 getUser
方法:
在前面的代码中,我们在现有方法中使用了 WebRequest
参数。 WebRequest
对象将用于调用 checkNotModified
方法。首先,我们通过 id
获取用户详细信息,并以毫秒为单位获取 updatedDate
。我们根据客户端标头信息检查用户更新日期(我们假设客户端将在标头中传递 If-Not-Modified-Since
)。如果用户更新日期比缓存日期新,我们假设用户已更新,因此我们将不得不发送新资源。
如果用户在缓存(由客户端)日期之后没有更新,它将简单地返回 null。此外,我们提供了足够的记录器来打印我们想要的语句。
让我们在 SoapUI 或 Postman 客户端中测试 REST API。当我们第一次调用API时,它会返回带有头信息的数据,如下:
SoapUI 客户端
我们可以看到我们正在使用该 API 的 GET
方法和右侧的响应标头。
Note
在前面的屏幕截图中,我们使用了端口 8081
。默认情况下,Spring Boot 在端口 8080
上工作。如果要改成8081
,在/src/main/resources/
application.properties
如下:server.port = 8081
如果下面没有application.properties
提到位置,您可以创建一个。
响应 (JSON) 如下所示:
在前面的 JSON 响应中,我们可以看到 user 详细信息,包括 updatedDate
。
响应(标头)如下:
在前面的响应头中,我们可以看到 HTTP 结果 200
(表示 OK)和 Last-Modified
日期。
现在,我们将在标题中添加 If-Modified-Since
并更新我们从上一个响应中获得的最新日期。我们可以检查以下屏幕截图中的 If-Modified-Since
参数:
在前面的配置中,我们在 header 部分添加了 If-Modified-Since
参数,并再次调用了相同的 REST API。该代码将检查自上次缓存日期以来资源是否已更新。在我们的例子中,资源没有更新,所以它只会在响应中返回 304
。我们可以看到如下响应:
HTTP 304
(未修改)响应只是向客户端传达没有修改的资源,因此客户端可以使用现有缓存。
如果我们通过调用更新 REST API (http://localhost:8081/user/100
using PUT
来更新指定用户) 然后调用前面的API(http://localhost:8081/user/100
using GET
),我们会得到在客户端缓存之后更新用户时的新资源。
在上一节中,我们根据更新日期探索了缓存。但是,当我们需要检查更新的资源时,我们可能并不总是需要依赖更新的日期。还有另一种机制,称为 ETag 缓存,它提供了一个强大的验证器来检查资源是否已更新。通过检查更新日期,ETag 缓存将成为常规缓存的完美替代方案。
在 ETag 缓存中,响应标头将提供正文的散列 ID (MD5)。如果资源被更新,标头将在 REST API 调用上生成一个新的哈希 ID。所以我们不需要像上一节那样明确检查信息。
Spring 提供了一个名为 ShallowEtagHeaderFilter
的过滤器来支持 ETag 缓存。让我们尝试在现有应用程序中添加 ShallowEtagHeaderFilter
。我们将在我们的主应用程序文件 (TicketManagementApplication
) 中添加代码:
在前面的代码中,我们将 ShallowEtagHeaderFilter
添加为 bean,并通过提供我们的 URL 模式和名称进行注册。由于我们现在只测试用户资源,我们将在我们的模式中添加 /user/*
。最后,我们的主要应用程序类将如下所示:
我们可以通过调用用户 API(http://localhost:8081/user
)来测试这个 ETag 机制。当我们调用这个 API 时,服务器会返回以下 headers:
我们可以看到 ETag
被添加到带有哈希 ID 的头部中。现在我们将使用带有散列值的 If-None-Match
标头调用相同的 API。我们将在以下屏幕截图中看到标题:
当我们再次使用 If-None-Match
标头和我们之前哈希 ID 的值调用相同的 API 时,服务器将返回 304
状态,我们可以看到如下:
在这种机制下,实际的响应正文不会发送 给客户端。相反,它会告诉客户端资源没有被修改,因此客户端可以使用之前缓存的内容。 304
状态表示资源未缓存。
在本章中,我们学习了提高应用程序性能的 HTTP 优化方法。通过减少客户端和服务器之间的交互以及 HTTP 上的数据大小,我们将在 REST API 服务中实现最高性能。我们将在 第 11 章,缩放中探索其他优化、缓存和缩放技术, 因为我们将讨论与 Web 服务性能相关的更高级主题。