跳转至

第28章:缓存

无论你是在创建一个JSON API,还是在构建一个iOS应用,甚至是在设计一个CPU的电路,你最终都会需要一个缓冲区。缓存--发音为cashes--是一种加速缓慢进程的方法,如果没有它们,互联网将是一个非常缓慢的地方。缓存背后的理念很简单。储存一个缓慢进程的结果,这样你只需要运行一次。在构建网络应用时,你可能会遇到一些慢进程的例子。

  • 大型数据库查询
  • 对外部服务的请求,例如,其他API
  • 复杂的计算,例如,解析一个大文件

通过缓存这些缓慢进程的结果,你可以使你的应用程序感觉更敏捷,反应更快。

缓存存储

Vapor定义了协议Cache。该协议为不同的缓存存储方法创建了一个通用接口。该协议本身很简单,看一下吧:

public protocol Cache {
  // 1
  func get<T>(_ key: String, as type: T.Type) -> EventLoopFuture<T?>
    where T: Decodable

  // 2
  func set<T>(_ key: String, to value: T?) -> EventLoopFuture<Void>
    where T: Encodable
}

下面是每种方法的作用:

  1. get(_:as:)从缓存中获取指定键的存储数据。如果该键没有数据存在,则返回nil
  2. set(_:to:)在缓存中存储所提供键的数据。如果之前有一个值存在,它将被替换。如果nil,则清除该键。

每个方法都返回一个未来,因为与缓存的交互可能是异步进行的。

现在你已经理解了缓存的概念和Cache协议,现在是时候看看Vapor的一些实际缓存实现了。

内存中的缓存

Vapor自带一个内存缓存。.memory。这个缓存将其数据存储在你的程序的运行内存中。这使得它非常适合于开发和测试,因为它没有外部依赖性。然而,它可能并不适合所有的用途,因为当应用程序重新启动时,存储被清除,并且不能在你的应用程序的多个实例之间共享。但最有可能的是,这种内存的不稳定性不会影响一个经过深思熟虑的缓存设计。

线程安全

内存缓存的内容是在你的应用程序的所有事件循环中共享的。这意味着一旦有东西被存储在缓存中,所有未来的请求都会看到相同的项目,无论它们被分配到哪个事件循环。为了实现这种跨循环的共享,内存缓存使用一个应用程序范围内的锁来同步访问。

数据库缓存

Vapor的缓存协议支持使用配置的数据库作为你的缓存存储。这包括Vapor的所有Fluent映射(PostgreSQLMySQLSQLiteMongoDB等)。

如果你想让你的缓存数据在重启之间持续存在,并且可以在你的应用程序的多个实例之间共享,那么将其存储在数据库中是一个不错的选择。如果你已经为你的应用程序配置了一个数据库,它很容易设置。

你可以使用你的应用程序的主数据库进行缓存,也可以使用一个单独的、专门的数据库。

Redis

Redis是一个开源的缓存存储服务。它通常被用作网络应用程序的缓存数据库,并被大多数部署服务如Heroku所支持。Redis数据库通常非常容易配置,它允许你在应用程序重新启动之间坚持你的缓存数据,并在你的应用程序的多个实例之间共享缓存。Redis是一个伟大的、快速的、功能丰富的内存缓存的替代品,它只需要多做一点配置。

现在你知道了Vapor中可用的缓存实现,是时候向应用程序添加缓存了。

例子:Pokédex

当构建一个网络应用时,向其他API发出请求会带来延迟。如果与你通信的API很慢,会让你的API感觉很慢。此外,外部API可能会对你在给定时间段内向其发出的请求数量实施速率限制。

幸运的是,通过缓存,你可以将这些外部API的查询结果存储在本地,使你的API感觉更快。

你将使用缓存来提高Pokédex的性能,这是一个用于存储和列出你所捕获的所有神奇宝贝的API

你已经学会了如何创建一个基本的CRUD API以及如何进行外部HTTP请求。因此,本章的启动项目已经实现了基本的功能。

在终端,切换到启动项目的目录,并使用以下命令来生成和打开一个Xcode项目来工作:

open Package.swift

概述

这个简单的Pokédex API有两条路由。

  • GET /pokemon: 返回所有捕获的口袋妖怪的列表。
  • POST /pokemon: 在Pokédex中存储一个捕获的神奇宝贝。

当你存储一个新的神奇宝贝时,Pokédex API会调用外部API pokeapi.co来验证你输入的神奇宝贝的名字是否真实。虽然这种检查是有效的,但pokeapi.co API的反应可能相当慢,从而使你的应用程序感觉很慢。

正常请求

在本地工作时,一个典型的Vapor请求只需要几毫秒的时间来响应。在下面的截图中,你可以看到GET /pokemon路线的总响应时间约为40ms

img

PokeAPI依赖请求

在下面的截图中,你可以看到POST /pokemon路线慢了25倍,大约为1500ms。这是因为pokeapi.co API对查询的反应可能很慢。

img

现在你准备看一下代码,以更好地了解是什么让这条路线变得缓慢,以及如何用缓存来解决这个问题。

验证名称

Xcode中,打开PokeAPI.swift并查看verify(name:)

这个类是HTTP客户端的一个简单包装,使查询PokeAPI更加方便。它使用verify(name:)来验证提供的神奇宝贝名字的合法性。如果名字是真实的,该方法返回true,并被包装成一个未来。

现在看看fetchPokemon(named:)。这个方法将请求发送到外部的pokeapi.co并返回神奇宝贝的数据。如果提供名称的神奇宝贝不存在,API--也就是这个方法--会返回一个404 Not Found的响应。

fetchPokemon(named:)是导致POST /pokemon路线的缓慢响应时间的原因。缓存正是医生所要求的!

创建一个缓冲区

第一个任务是为PokeAPI包装器创建一个缓存。在PokeAPI.swift中,添加一个新的属性来存储缓存,在let client: Client

/// Cache to check before calling API.
let cache: Cache

接下来,替换init的实现,以考虑到新的属性:

public init(client: Client, cache: Cache) {
  self.client = client
  self.cache = cache
}

最后,通过将文件顶部的Request扩展名替换为以下内容来修复剩余的编译器错误:

extension Request {
  public var pokeAPI: PokeAPI {
    .init(client: self.client, cache: self.cache)
  }
}

默认情况下,Vapor被配置为使用内置的内存缓存。

获取和存储

现在PokeAPI包装器可以访问工作中的Cache,你可以使用缓存来存储来自pokeapi.co API的响应,随后更快速地获取它们。

打开PokeAPI.swift,将verify(name:)重命名为uncachedVerify(name:)。接下来,添加下面的方法来替换未缓存的实现:

public func verify(name: String) -> EventLoopFuture<Bool> {
  // 1
  let name = name
    .lowercased()
    .trimmingCharacters(in: .whitespacesAndNewlines)

  // 2
  return cache.get(name, as: Bool.self).flatMap { verified in
    // 3
    if let verified = verified {
      return self.client.eventLoop.makeSucceededFuture(verified)
    } else {
      return self.uncachedVerify(name: name).flatMap {
        verified in
        // 4
        return self.cache.set(name, to: verified)
          .transform(to: verified)
      }
    }
  }
}

下面是这个的作用:

  1. 通过小写名字创建一个一致的缓存键。这可以确保Pikachupikachu共享同一个缓存结果。
  2. 查询缓存,看它是否包含所需的结果。
  3. 如果存在一个缓存的结果,则返回该结果。这意味着对verify(name:)的调用将永远不会为一个给定的名字第二次调用fetchPokemon(named:)。这是提高性能的关键步骤。
  4. fetchPokemon(named:)完成后,将API查询的结果存储在缓存中。

建立并运行,然后在RESTed中创建一个新的请求。配置该请求如下:

  • URL: http://localhost:8080/pokemon
  • method: POST
  • Parameter encoding: JSON-encoded

添加一个带有名称和值的参数:

  • name: Test

注意第一次请求的响应时间。它可能是几秒钟。现在,提出第二个请求并记下时间;它应该快得多!

img

Fluent

一旦你将你的应用程序配置为使用Vapor的缓存接口,就很容易换掉底层的实现。由于这个应用程序已经使用SQLite来存储捕获的Pokémon,你可以很容易地启用Fluent作为一个缓存。与内存缓存不同的是,Fluent缓存可以在你的应用程序的多个实例之间共享,并且在重新启动时持续存在。

要将Vapor的缓存实现切换为使用Fluent,请打开configure.swift并添加以下内容:

app.caches.use(.fluent)

就在这条线上面:

try routes(app)

最后,由于Fluent目前被配置为使用SQL数据库(SQLite),所以需要准备存储缓存值。还是在configure.swift里面,找到:

app.migrations.add(CreatePokemon())

并在其下方添加以下迁移:

app.migrations.add(CacheEntry.migration)

现在你应该注意到,在应用程序重新启动之间,缓存的值是持久的。很好!

接下来去哪?

缓存是计算机科学中的一个重要概念,了解如何使用它将有助于使你的网络应用感到快速和响应。有几种方法来存储网络应用的缓存数据:内存、Fluent数据库、Redis等等。相比之下,每一种都有明显的好处。

你可以查看不同类型的缓存算法,如最近最少使用(LRU)、随机替换(RR)或后进先出(LIFO)。每种算法都有其优点和缺点,这取决于你所编写的应用程序的类型以及你在其中缓存的数据类型。

在本章中,你学到了如何配置Fluent数据库缓存。使用缓存来保存对外部API的请求结果,你大大增加了应用程序的响应速度。

如果你想挑战一下,可以尝试配置你的应用程序来使用Redis缓存。但请记住,你必须把它们都缓存起来