第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
}
下面是每种方法的作用:
get(_:as:)
从缓存中获取指定键的存储数据。如果该键没有数据存在,则返回nil
。set(_:to:)
在缓存中存储所提供键的数据。如果之前有一个值存在,它将被替换。如果nil
,则清除该键。
每个方法都返回一个未来,因为与缓存的交互可能是异步进行的。
现在你已经理解了缓存的概念和Cache
协议,现在是时候看看Vapor
的一些实际缓存实现了。
内存中的缓存¶
Vapor
自带一个内存缓存。.memory
。这个缓存将其数据存储在你的程序的运行内存中。这使得它非常适合于开发和测试,因为它没有外部依赖性。然而,它可能并不适合所有的用途,因为当应用程序重新启动时,存储被清除,并且不能在你的应用程序的多个实例之间共享。但最有可能的是,这种内存的不稳定性不会影响一个经过深思熟虑的缓存设计。
线程安全¶
内存缓存的内容是在你的应用程序的所有事件循环中共享的。这意味着一旦有东西被存储在缓存中,所有未来的请求都会看到相同的项目,无论它们被分配到哪个事件循环。为了实现这种跨循环的共享,内存缓存使用一个应用程序范围内的锁来同步访问。
数据库缓存¶
Vapor
的缓存协议支持使用配置的数据库作为你的缓存存储。这包括Vapor
的所有Fluent
映射(PostgreSQL
、MySQL
、SQLite
、MongoDB
等)。
如果你想让你的缓存数据在重启之间持续存在,并且可以在你的应用程序的多个实例之间共享,那么将其存储在数据库中是一个不错的选择。如果你已经为你的应用程序配置了一个数据库,它很容易设置。
你可以使用你的应用程序的主数据库进行缓存,也可以使用一个单独的、专门的数据库。
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
。
PokeAPI
依赖请求¶
在下面的截图中,你可以看到POST /pokemon
路线慢了25
倍,大约为1500ms
。这是因为pokeapi.co API
对查询的反应可能很慢。
现在你准备看一下代码,以更好地了解是什么让这条路线变得缓慢,以及如何用缓存来解决这个问题。
验证名称¶
在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)
}
}
}
}
下面是这个的作用:
- 通过小写名字创建一个一致的缓存键。这可以确保
Pikachu
和pikachu
共享同一个缓存结果。 - 查询缓存,看它是否包含所需的结果。
- 如果存在一个缓存的结果,则返回该结果。这意味着对
verify(name:)
的调用将永远不会为一个给定的名字第二次调用fetchPokemon(named:)
。这是提高性能的关键步骤。 - 当
fetchPokemon(named:)
完成后,将API
查询的结果存储在缓存中。
建立并运行,然后在RESTed中创建一个新的请求。配置该请求如下:
- URL: http://localhost:8080/pokemon
- method: POST
- Parameter encoding: JSON-encoded
添加一个带有名称和值的参数:
- name: Test
注意第一次请求的响应时间。它可能是几秒钟。现在,提出第二个请求并记下时间;它应该快得多!
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
缓存。但请记住,你必须把它们都缓存起来