vlambda博客
学习文章列表

读书笔记《building-restful-web-services-with-spring-5-second-edition》性能

Chapter 8. Performance

当涉及到应用程序中的 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

HTTP compression


为了从 REST 服务快速获取内容,可以通过 HTTP 等协议压缩和发送数据。在压缩数据时,我们必须遵循一些编码格式,因此接收端将应用相同的格式。

Content negotiation

请求服务器中的资源时,客户端将有许多选项来接收各种表示形式的内容。例如,DOC/PDF 是数据类型表示。土耳其语或英语是语言表示,服务器可以在其中以特定语言发送资源。服务端和客户端之间必须就资源将被访问的格式达成某种协议,例如语言、数据类型等。该过程称为内容协商

在这里,我们将讨论两种不同的内容协商机制:服务器驱动和代理驱动机制。在继续讨论这些机制之前,我们将讨论 Accept-Encoding 和 Content-Encoding,因为它们很重要。

Accept-Encoding

客户端将告诉服务器它可以接收哪些 compression 算法。最常见的编码类型是 gzipdeflate。在请求服务器时,客户端将在请求标头中共享编码类型。接受编码将用于此类目的。简单地说,客户端会对服务器说“我只接受提到的压缩格式”。

我们将看到示例 Accept-Encoding 如下:

Accept-Encoding: gzip, deflate

在前面的标头中,客户端说它在响应中只能接受 gzipdeflate

其他可能的选项如下所述:

Accept-Encoding: compress, gzip
Accept-Encoding: 
Accept-Encoding: *
Accept-Encoding: compress;q=0.5, gzip;q=1.0
Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0

我们可以看到 compress 值后跟 q=0.5,这意味着质量等级只有 0.5q=1.0gzip 评级相比,这是非常高的。在这种情况下,客户端建议服务器可以通过 compress 获取 gzip。但是,如果 gzip 是不可能的,则 compress 对客户端来说很好。

如果服务器不支持客户端请求的压缩算法,则服务器应发送带有 406 (Not Acceptable) 状态码的错误响应。

Content-Encoding

Content-Encoding 是一个实体头,使用 压缩要从服务器发送到客户端的数据类型。 Content-Encoding 值告诉客户端在实体主体中使用了哪些编码。它将告诉客户端如何解码数据以检索值。

让我们看一下单个和多个编码选项:

// Single Encoding option
Content-Encoding: gzip
Content-Encoding: compress

// Multiple Encoding options
Content-Encoding: gzip, identity
Content-Encoding: deflate, gzip

在上述配置中,Content-Encoding 提供了单选项和多选项。在这里,服务器告诉客户端它可以提供基于 gzipcompress 算法的编码。如果服务器提到了多重编码,这些编码将按照提到的顺序应用。

Note

强烈建议尽可能多地压缩数据。不建议即时更改内容编码。由于它将折叠未来的请求(例如 GET 上的 PUT),动态更改 Content-Encoding 不是一个好主意一点也不。

Server-driven content negotiation

服务器驱动的内容协商由服务器端算法执行,以决定服务器必须发送给客户。它也称为主动内容协商。在服务器驱动的协商中,客户端(用户代理)将给出各种具有质量评级的表示选项。服务器中的算法必须决定哪种表示最适合客户端提供的标准。

例如,客户端通过共享媒体类型标准来请求资源,并带有诸如哪种媒体类型更适合客户端的评级。服务器将完成其余的工作,并提供适合客户需求的最佳资源表示。

Agent-driven content negotiation

代理驱动的内容协商由客户端的算法执行。当客户端请求特定资源时,服务器将告诉客户端资源的各种表示形式,包括内容类型、质量等元数据。然后客户端算法将决定哪个是最好的,并再次从服务器请求它。这也称为反应式内容协商。

HTTP caching


当客户端多次请求相同的 resource 表示时,从服务器端提供它会浪费时间这在 Web 应用程序中会很耗时。如果资源被重用,而不是与服务器对话,它肯定会提高 Web 应用程序的性能。

缓存将被视为为我们的 Web 应用程序带来性能的主要选项。 Web缓存避免服务器多次接触,减少延迟;因此,应用程序会更快。缓存可以应用于应用程序的不同层。在本章中,我们将只讨论 HTTP 缓存,它被认为是一个中间层。我们将在 第 11 章缩放中深入研究其他形式的缓存>

HTTP cache control

缓存控件是一个标头field,它指定了Web上缓存操作的指令。这些指令给予缓存授权,定义缓存的持续时间,等等。指令定义行为,通常旨在防止缓存响应。

在这里,我们将讨论 HTTP 缓存指令:publicprivateno-cache only-if-cached 指令。

Public caching

如果缓存控件允许公共缓存,则 resource 可以被多个用户缓存缓存。我们可以通过在 Cache-Control 标头中设置 public 选项来做到这一点。在公共缓存中,响应可能会被多个用户缓存,即使是不可缓存或可缓存的,也只能在非共享缓存中进行缓存:

Cache-Control: public

上述设置中,public表示响应可以被任意缓存缓存。

Private caching

与公共缓存不同,私有 responses 适用于单个用户缓存,而不适用于共享缓存。在私有缓存中,中间体不能缓存内容:

Cache-Control: private

前面的设置表明响应只对单个用户可用,不应被任何其他缓存访问。

此外,我们可以在标题设置中指定内容应缓存多长时间。这可以通过 max-age 指令选项来完成。

检查以下设置:

Cache-Control: private, max-age=600

在前面的设置中,我们提到了可以在私有模式下缓存响应(仅限单用户)以及资源被认为是新鲜的最长时间。

No-cache

访问动态资源可能不需要缓存。在这种情况下,我们可以在缓存控件中使用 no-cache 设置来避免客户端缓存:

Cache-Control: no-cache

The preceding setting will tell the client to check the server whenever the resource is being requested.

此外,在某些情况下,我们可能需要禁用缓存机制本身。这可以在我们的设置中使用 no-store 来完成:

Cache-Control: no-store

上述设置将告诉客户端避免资源缓存并始终从服务器获取资源。

Note

HTTP/1.0 缓存不会遵循 no-cache 指令,因为它是在 HTTP/1.1 中引入的 缓存控制仅在 HTTP/1.1 中引入。在 HTTP /1.0 中,只有 Pragma: no-cache 用于防止响应被缓存。

Only-if-cached

在某些情况下,例如网络连接不佳,客户端可能希望返回缓存的 resource 而不是重新加载或重新验证服务器.为此,客户端可以在请求中包含 only-if-cached 指令。如果收到,客户端将获取缓存条目,否则以 504(网关超时)状态响应。

Note

这些缓存控制指令可以覆盖默认缓存算法。

到目前为止,我们已经讨论了各种缓存控制指令及其解释。以下是缓存请求和缓存响应指令的示例设置。

请求缓存控制指令(标准的Cache-Control指令,客户端可以在HTTP请求中使用)如下:

Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-Control: no-cache 
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: only-if-cached

响应缓存控制指令(标准 Cache-Control 指令,服务器可以在 HTTP 响应中使用)如下:

Cache-Control: must-revalidate
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: no-transform
Cache-Control: public
Cache-Control: private
Cache-Control: proxy-revalidate
Cache-Control: max-age=<seconds>
Cache-Control: s-maxage=<seconds>

Note

无法为特定缓存指定缓存指令。

Cache validation

当缓存有一个新的 entry 可以在客户端请求时用作响应时,它将与原始服务器进行检查查看缓存的条目是否仍然可用。此过程称为缓存验证。此外,当用户按下重新加载按钮时,会触发重新验证。如果缓存响应中包含Cache-Control: must revalidate头,正常浏览下会触发。

当资源的时间到期时,它将被 validated 或再次获取。仅当服务器提供强或弱验证器时才会触发缓存验证。

ETags

ETags 提供了一个机制 来验证缓存的响应。 ETag 响应标头可用作强验证器。在这种情况下,客户既无法理解价值,也无法预测其价值。当服务器发出响应时,它会生成一个隐藏资源状态的令牌:

ETag : ijk564

如果 ETag 是响应的一部分,则客户端可以在未来请求的标头中发出 If-None-Match验证缓存的资源:

If-None-Match: ijk564

服务器会将请求的标头与资源的当前状态进行比较。如果资源状态发生变化,服务器将响应一个新资源。否则,服务器将返回 304 Not Modified 响应。

Last-Modified/If-Modified-Since headers

到目前为止,我们已经看到了一个强大的验证器(ETags)。在这里,我们将讨论一个 可以 在标头中使用的弱验证器。 Last-Modified 响应标头可用作弱验证器。不是生成资源的哈希,而是使用时间戳来检查缓存的响应是否有效。

由于此验证器的分辨率为 1 秒,因此与 ETags 相比,它被认为是弱的。如果响应中存在 Last-Modified 标头,则客户端可以发送 If-Modified-Since 请求标头到验证缓存的资源。

If-Modified-Since 标头在请求资源时由客户端提供。为了简化实际示例中的机制,客户端请求将类似于:“我已经在上午 10 点缓存了资源 XYZ;但是,如果它从上午 10 点开始更改,则获取更新的 XYZ。否则只返回 304。那我就用之前缓存的XYZ。”

Cache implementation


到目前为止,我们已经在 this 章节中看到了理论部分。让我们尝试在我们的应用程序中实现这个概念。为了简化缓存实现,我们将只使用用户管理。我们将使用 getUser(单用户)REST API 来应用我们的缓存概念。

The REST resource

getUser 方法中,我们将正确的 userid 传递给路径变量,假设客户端将传递 userid 并获取资源。有许多缓存选项可供实现。在这里,我们将只使用 If-Modified-Since 缓存机制。由于这个机制会在header中传递If-Modified-Since的值,然后转发给服务器,表示如果资源在指定时间后发生变化,则获取资源新鲜,否则返回 null。

我们可以通过多种方式实现缓存。由于我们的目标是简化和清楚地传达信息,我们将保持代码简单,而不是增加代码的复杂性。为了实现 this 缓存,我们可能需要添加一个名为 updatedDate< /code> 在我们的 User 类中。让我们在我们的类中添加变量。

updatedDate 变量将用作 If-Modified-Since 缓存的检查变量,因为我们将依赖 user-updated日期。

客户端将询问服务器自上次缓存时间以来用户数据是否已更改。服务器将检查用户updatedDate,如果没有更新则返回null;否则,否则它将返回新数据:

  private Date updatedDate;
  public Date getUpdatedDate() {
    return updatedDate;
  }
  public void setUpdatedDate(Date updatedDate) {
    this.updatedDate = updatedDate;
  }

在前面的代码中,我们刚刚添加了一个新变量 updatedDate,并在其中添加了正确的 getter 和 setter 方法。稍后我们可能会通过添加 Lombok 库来清理这些 getter 和 setter 方法。我们将在接下来的章节中应用 Lombok。

另外,当我们获得类的实例时,我们需要添加另一个构造函数来初始化 updatedDate 变量。让我们在这里添加构造函数:

public User(Integer userid, String username, Date updatedDate){
    this.userid = userid;
    this.username = username;
    this.updatedDate = updatedDate;
  }

如果可能,我们可以将 toString 方法更改如下:

  @Override
  public String toString() {
    return "User [userid=" + userid + ", username=" + username + ", updatedDate=" + updatedDate + "]";
  }

添加前面提到的所有细节后,我们的类将如下所示:

package com.packtpub.model;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {  
  private static final long serialVersionUID = 1L;
  public User() {
  }
  private Integer userid;
  private String username;
  private Date updatedDate;
  public User(Integer userid, String username) {
    this.userid = userid;
    this.username = username;
  }
  public User(Integer userid, String username, Date updatedDate) {
    this.userid = userid;
    this.username = username;
    this.updatedDate = updatedDate;
  }
  public Date getUpdatedDate() {
    return updatedDate;
  }
  public void setUpdatedDate(Date updatedDate) {
    this.updatedDate = updatedDate;
  }
  public Integer getUserid() {
    return userid;
  }
  public void setUserid(Integer userid) {
    this.userid = userid;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  @Override
  public String toString() {
    return "User [userid=" + userid + ", username=" + username + ", updatedDate=" + updatedDate + "]";
  }
}

现在,我们将回到我们在前面章节中介绍的 UserController,并更改 getUser 方法:

@RestController
@RequestMapping("/user")
public class UserController {
    // other methods and variables (hidden)  
    @ResponseBody
    @RequestMapping("/{id}")
    public User getUser(@PathVariable("id") Integer id, WebRequest webRequest){    
        User user = userSevice.getUser(id);
        long updated = user.getUpdatedDate().getTime();    
        boolean isNotModified = webRequest.checkNotModified(updated);    
        logger.info("{getUser} isNotModified : "+isNotModified);    
        if(isNotModified){
          logger.info("{getUser} resource not modified since last call, so exiting");
          return null;
        }    
        logger.info("{getUser} resource modified since last call, so get the updated content");    
        return userSevice.getUser(id);
   }
}

在前面的代码中,我们在现有方法中使用了 WebRequest 参数。 WebRequest 对象将用于调用 checkNotModified 方法。首先,我们通过 id 获取用户详细信息,并以毫秒为单位获取 updatedDate。我们根据客户端标头信息检查用户更新日期(我们假设客户端将在标头中传递 If-Not-Modified-Since)。如果用户更新日期比缓存日期新,我们假设用户已更新,因此我们将不得不发送新资源。

Note

我们可能必须导入 org.apache.log4j.Logger,因为我们在 UserController 中添加了记录器。否则编译时会报错。

如果用户在缓存(由客户端)日期之后没有更新,它将简单地返回 null。此外,我们提供了足够的记录器来打印我们想要的语句。

让我们在 SoapUI 或 Postman 客户端中测试 REST API。当我们第一次调用API时,它会返回带有头信息的数据,如下:

读书笔记《building-restful-web-services-with-spring-5-second-edition》性能

SoapUI 客户端

我们可以看到我们正在使用该 API 的 GET 方法和右侧的响应标头。

Note

在前面的屏幕截图中,我们使用了端口 8081。默认情况下,Spring Boot 在端口 8080 上工作。如果要改成8081,在/src/main/resources/application.properties 如下:server.port = 8081 如果下面没有application.properties提到位置,您可以创建一个。

响应 (JSON) 如下所示:

{
   "userid": 100,
   "username": "David",
   "updatedDate": 1516201175654
}

在前面的 JSON 响应中,我们可以看到 user 详细信息,包括 updatedDate

响应(标头)如下:

HTTP/1.1 200 
Last-Modified: Wed, 17 Jan 2018 14:59:35 GMT
ETag: "06acb280fd1c0435ac4ddcc6de0aeeee7"
Content-Type: application/json;charset=UTF-8
Content-Length: 61
Date: Wed, 17 Jan 2018 14:59:59 GMT

{"userid":100,"username":"David","updatedDate":1516201175654}

在前面的响应头中,我们可以看到 HTTP 结果 200(表示 OK)和 Last-Modified 日期。

现在,我们将在标题中添加 If-Modified-Since 并更新我们从上一个响应中获得的最新日期。我们可以检查以下屏幕截图中的 If-Modified-Since 参数:

读书笔记《building-restful-web-services-with-spring-5-second-edition》性能

在前面的配置中,我们在 header 部分添加了 If-Modified-Since 参数,并再次调用了相同的 REST API。该代码将检查自上次缓存日期以来资源是否已更新。在我们的例子中,资源没有更新,所以它只会在响应中返回 304。我们可以看到如下响应:

HTTP/1.1 304 
Last-Modified: Wed, 17 Jan 2018 14:59:35 GMT
Date: Wed, 17 Jan 2018 15:05:29 GMT

HTTP 304(未修改)响应只是向客户端传达没有修改的资源,因此客户端可以使用现有缓存。

如果我们通过调用更新 REST API (http://localhost:8081/user/100 using PUT 来更新指定用户) 然后调用前面的API(http://localhost:8081/user/100 using GET),我们会得到在客户端缓存之后更新用户时的新资源。

Caching with ETags

在上一节中,我们根据更新日期探索了缓存。但是,当我们需要检查更新的资源时,我们可能并不总是需要依赖更新的日期。还有另一种机制,称为 ETag 缓存,它提供了一个强大的验证器来检查资源是否已更新。通过检查更新日期,ETag 缓存将成为常规缓存的完美替代方案。

在 ETag 缓存中,响应标头将提供正文的散列 ID (MD5)。如果资源被更新,标头将在 REST API 调用上生成一个新的哈希 ID。所以我们不需要像上一节那样明确检查信息。

Spring 提供了一个名为 ShallowEtagHeaderFilter 的过滤器来支持 ETag 缓存。让我们尝试在现有应用程序中添加 ShallowEtagHeaderFilter。我们将在我们的主应用程序文件 (TicketManagementApplication) 中添加代码:

  @Bean
  public Filter shallowEtagHeaderFilter() {
    return new ShallowEtagHeaderFilter();
  }
  @Bean
  public FilterRegistrationBean shallowEtagHeaderFilterRegistration() {
    FilterRegistrationBean result = new FilterRegistrationBean();
    result.setFilter(this.shallowEtagHeaderFilter());
    result.addUrlPatterns("/user/*");
    result.setName("shallowEtagHeaderFilter");
    result.setOrder(1);
    return result;
  }

在前面的代码中,我们将 ShallowEtagHeaderFilter 添加为 bean,并通过提供我们的 URL 模式和名称进行注册。由于我们现在只测试用户资源,我们将在我们的模式中添加 /user/*。最后,我们的主要应用程序类将如下所示:

package com.packtpub.restapp.ticketmanagement;
import javax.servlet.Filter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
@ComponentScan("com.packtpub")
@SpringBootApplication
public class TicketManagementApplication {
  public static void main(String[] args) {
    SpringApplication.run(TicketManagementApplication.class, args);
  }
  @Bean
  public Filter shallowEtagHeaderFilter() {
    return new ShallowEtagHeaderFilter();
  }
  @Bean
  public FilterRegistrationBean shallowEtagHeaderFilterRegistration() {
    FilterRegistrationBean result = new FilterRegistrationBean();
    result.setFilter(this.shallowEtagHeaderFilter());
    result.addUrlPatterns("/user/*");
    result.setName("shallowEtagHeaderFilter");
    result.setOrder(1);
    return result;
  }
}

我们可以通过调用用户 API(http://localhost:8081/user)来测试这个 ETag 机制。当我们调用这个 API 时,服务器会返回以下 headers:

HTTP/1.1 200 
ETag: "02a4bc8613aefc333de37c72bfd5e392a"
Content-Type: application/json;charset=UTF-8
Content-Length: 186
Date: Wed, 17 Jan 2018 15:11:45 GMT 

我们可以看到 ETag 被添加到带有哈希 ID 的头部中。现在我们将使用带有散列值的 If-None-Match 标头调用相同的 API。我们将在以下屏幕截图中看到标题:

读书笔记《building-restful-web-services-with-spring-5-second-edition》性能

当我们再次使用 If-None-Match 标头和我们之前哈希 ID 的值调用相同的 API 时,服务器将返回 304 状态,我们可以看到如下:

HTTP/1.1 304 
ETag: "02a4bc8613aefc333de37c72bfd5e392a"
Date: Wed, 17 Jan 2018 15:12:24 GMT 

在这种机制下,实际的响应正文不会发​​送 给客户端。相反,它会告诉客户端资源没有被修改,因此客户端可以使用之前缓存的内容。 304 状态表示资源未缓存。

Summary


在本章中,我们学习了提高应用程序性能的 HTTP 优化方法。通过减少客户端和服务器之间的交互以及 HTTP 上的数据大小,我们将在 REST API 服务中实现最高性能。我们将在 第 11 章缩放中探索其他优化、缓存和缩放技术, 因为我们将讨论与 Web 服务性能相关的更高级主题。