本章将展示基于actor模型(如Erlang或Akka)创建微服务的替代方法。 这种方法允许您通过将微服务拆分为通过消息传递相互交互的小型独立任务来编写清晰有效的代码。
到本章结束时,您将能够执行以下操作:
- 使用Actix框架和actix-web包创建一个微服务
- 为Actix Web框架创建中间件
技术要求
要实现并运行本章的所有示例,您至少需要使用版本为1.31的Rust编译器。
您可以在GitHub的本章中找到代码示例的源代码:https://github.com/PacktPublishing/Hands-On-Microservices-with-Rust/tree/master/Chapter11
微服务中的Actor并发
如果您熟悉Erlang或Akka,您可能已经知道Actor是什么以及如何使用它们。 但无论如何,我们将在本节中更新我们对actor模型的了解。
了解actors
我们已经熟悉了第10章,后台任务和微服务中的线程池中的actor,但我们来谈谈使用actor来进行微服务。
actor是进行并发计算的模型。 我们应该知道以下模型:
线程:在此模型中,每个任务都在一个单独的线程中工作
光纤或绿色线程:在此模型中,每个任务都由特殊运行时调度
异步代码:在这个模型中,每个任务都由一个反应堆运行(实际上,这类似于光纤)
actors将所有这些方法组合成一个优雅的方法。 要完成工作的任何部分,您可以实现执行自己工作的角色,并通过消息与其他角色互动,以便相互通知整体进度。 每个actor都有一个邮箱用于传入消息,并可以使用此地址向其他actor发送消息。
微服务中的actors
要使用actor开发微服务,您应该将服务拆分为可以解决不同类型工作的任务。例如,您可以为每个传入的连接或数据库交互使用单独的actor,甚至可以作为控制其他actor的主管。每个actor都是在reactor中执行的异步任务。
这种方法的好处如下:
编写单独的actor比使用大量函数更简单
演员可以失败并重生
你可以重用actor
使用actor的一个重要好处是可靠性,因为每个actor都可能失败并重生,因此您不需要很长的恢复代码来处理失败。这并不意味着你的代码可以召唤恐慌!宏无处不在,但这确实意味着您可以将actor视为在小任务上同时工作的短生命周期任务。
如果你设计好actors,你也会获得很好的表现,因为与信息的互动有助于你将工作分成短暂的反应,这不会长时间阻挡反应堆。此外,您的源代码变得更加结构化。
Actix框架
Actix框架为Rust提供了一个actor模型,它基于futures create和一些异步代码,允许actor以最少的资源同时工作。
我认为这是使用Rust创建Web应用程序和微服务的最佳工具之一。 该框架包括两个好的包 - 包含核心结构的actix包,以及实现HTTP和WebSocket协议的actix-web包。 让我们创建一个微服务,将请求路由到其他微服务。
使用actix-web创建微服务
在本节中,我们将创建一个类似于我们在第9章“简单REST定义和请求使用框架的路由”中创建的其他微服务的微服务,但在内部使用actor模型来实现完全的资源利用率。
要使用actix web创建微服务,您需要添加actix和actix-web包。 首先,我们需要启动管理其他actor的运行时的System actor。 让我们创建一个System实例并启动一个带有必要路由的actix-web服务器。
引导actix-web服务器
启动actix-web服务器实例看起来与其他Rust Web框架类似,但需要System actor实例。 我们不需要直接使用System,但需要在一切准备就绪时通过调用run方法来运行它。 此调用启动System actor并阻止当前线程。 在内部,它使用我们在前面章节中讨论过的block_on方法。
启动服务器
请考虑以下代码:
fn main() {
env_logger::init();
let sys = actix::System::new("router");
server::new(|| { // Insert `App` declaration here }).workers(1) .bind("127.0.0.1:8080") .unwrap() .start();
let _ = sys.run();}
我们使用server :: new方法调用创建一个新服务器,该调用期望闭包返回App实例。 在我们创建App实例之前,我们必须完成我们的服务器并运行它。 workers方法设置运行actor的线程数。
您可以选择不显式设置此值,默认情况下,它将等于系统上的CPU数。 在许多情况下,它是性能的最佳价值。
下一次调用bind方法会将服务器的套接字绑定到一个地址。 如果它不能绑定到地址,则该方法返回Err,如果我们无法在所需端口上启动服务器,则会打开结果以暂停服务器。 最后,我们调用start方法来启动Server actor。 它返回一个Addr结构,其中包含一个可用于将消息发送到Server actor实例的地址。
实际上,在我们调用运行System实例的方法之前,Server actor不会运行。 添加此方法调用,然后我们将继续详细介绍如何创建App实例。
应用创建
将以下代码插入server :: new函数调用的闭包中:
let app = App::with_state(State::default())
.middleware(middleware::Logger::default())
.middleware(
IdentityService::new(CookieIdentityPolicy::new(&[0; 32]) .name("auth-example").secure(false), ))
.middleware(Counter);
App结构包含有关状态,中间件和路由范围的信息。 要为我们的应用程序设置共享状态,我们使用with_state方法来构造App实例。 我们创建State结构的默认实例,声明如下:
#[derive(Default)]
struct State(RefCell<i64>);
State包含一个具有i64值的单元格,用于计算所有请求。默认情况下,它使用0值创建。在此之后,我们使用App的中间件方法来设置以下三个中间件:
- actix_web :: middleware :: Logger是一个记录器,它使用日志包来记录请求和响应
- actix_web :: middleware :: identity :: IdetityService使用实现IdentityPolicy特征的身份后端帮助识别请求
- Counter是一个中间件,我们将在以下中间件部分创建,并使用State来计算请求的总数量
对于我们的IdentityPolicy后端,我们使用IdentityService所在的同一身份子模块中的CookieIdentityPolicy。 CookieIdentityPolicy需要一个至少包含32个字节的密钥。当创建了cookie的身份策略实例时,我们可以使用路径,名称和域等方法来设置特定的cookie参数。我们还允许使用带有false值的安全方法发送具有不安全连接的cookie。
您应该了解的cookie有两个特殊参数: Secure和HttpOnly。 第一个需要安全的HTTPS连接来发送cookie。 如果您运行服务进行测试并使用纯HTTP连接到它,那么CookieIdentityPolicy将不起作用。 HttpOnly参数不允许使用JavaScript中的cookie。 CookieIdentityPolicy将此参数设置为true,您无法覆盖此行为。
范围和路由
接下来我们要添加到App实例中的是路由。有一个路由功能,可以让您为任何路由设置处理程序。但是使用范围来构造嵌套路径的结构更为周到。看下面的代码:
app.scope("/api", |scope| {
scope .route("/signup", http::Method::POST, signup)
.route("/signin", http::Method::POST, signin)
.route("/new_comment", http::Method::POST, new_comment)
.route("/comments", http::Method::GET, comments)})
我们的App结构的scope方法需要一个路径的前缀和一个带作为单个参数的闭包,并创建一个可以包含子路由的作用域。我们为/ api路径前缀创建一个范围,并使用route方法添加四个路由:/ signup,/ signin,/ new_comment和/ comments。 route方法需要一个后缀,包括路径,方法和处理程序。例如,如果服务器现在使用POST方法请求/ api / singup,它将调用注册函数。让我们为其他路径添加一个默认处理程序。
我们的微服务还使用Counter中间件,我们将在稍后实现,以计算请求的总数量。我们需要为微服务添加一个呈现统计信息的路由,如下所示:
.route("/counter", http::Method::GET, counter)
如您所见,我们这里不需要范围,因为我们只有一个处理程序,可以直接为App实例(不是范围)调用route方法。
静态文件处理程序
对于之前范围中未列出的其他路径,我们将使用一个处理程序,该处理程序将从文件夹返回文件的内容以提供静态资产。 处理程序方法需要路径的前缀和实现Handler特征的类型。 在我们的例子中,我们将使用一个现成的静态文件处理程序actix_web :: fs :: StaticFiles。 它需要一个本地文件夹的路径,我们还可以通过调用index_file方法来设置索引文件:
app.handler( "/", fs::StaticFiles::new("./static/").unwrap().index_file("index.html"))
现在,如果客户端向诸如/index.html或/css/styles.css的路径发送GET请求,则StaticFiles处理程序将从./static/ local文件夹发送相应文件的内容。
HTTP客户端
这个微服务的处理程序作为代理工作,并将传入的请求重新发送到其他微服务,这些服务将无法直接供用户使用。 要将请求发送到其他微服务,我们需要一个HTTP客户端。 actix_web包含一个。 要使用客户端,我们添加两个函数:一个用于代理GET请求,另一个用于发送POST请求。
GET请求
要发送GET请求,我们创建一个get_request函数,该函数需要一个url参数并返回一个包含二进制数据的Future实例:
fn get_request(url: &str) -> impl Future<Item = Vec<u8>, Error = Error> { client::ClientRequest::get(url) .finish().into_future() .and_then(|req| { req.send() .map_err(Error::from) .and_then(|resp| resp.body().from_err()) .map(|bytes| bytes.to_vec()) })}
我们使用ClientRequestBuilder来创建ClientRequest实例。 ClientRequest结构已经有了一些快捷方式,可以使用预设的HTTP方法创建构建器。我们调用get方法只将Method :: GET值设置为一个请求,该请求是作为ClientRequestBuilder实例的调用方法实现的。您还可以使用构建器来设置额外的标头或Cookie。完成这些值后,必须通过调用以下方法之一从构建器创建ClientRequest实例:
body将body值设置为可以转换为的二进制数据
json将body值设置为可以序列化为JSON值的任何类型
form将body值设置为可以使用serde_urlencoded:serializer序列化的类型
流消耗Stream实例中的正文值
完成创建没有正文值的请求
我们使用finish,因为GET请求不包含body值。所有这些方法都返回一个Result,其中ClientRequest实例作为成功值。我们不打开Result并使用into_future方法调用将其转换为Future值,以便在处理程序甚至无法构建请求时将Error值返回给客户端。
由于我们有Future值,我们可以使用and_then方法添加下一个处理步骤。我们调用ClientRequest的send方法来创建SendRequest实例,该实例实现Future trait并向服务器发送请求。
由于send调用可以返回SendRequestError错误类型,因此我们用failure :: Error包装它。
如果请求已成功发送,我们可以使用body方法调用获取MessageBody值。此方法是HttpMessage特征的一部分。 MessageBody还使用Bytes值实现Future trait,我们使用and_then方法扩展future链并将值从SendRequest转换为Bytes。
最后,我们使用Bytes的to_vec方法将其转换为Vec ,并将此值作为对客户端的响应提供。我们已经完成了代理GET请求到另一个微服务的方法。让我们为POST请求创建一个类似的方法。
POST请求
对于POST请求,我们需要将序列化到请求主体的输入参数,以及将从请求的响应主体反序列化的输出参数。 看看以下功能:
fn post_request<T, O>(url: &str, params: T) -> impl Future<Item = O, Error = Error>where T: Serialize, O: for <'de> Deserialize<'de> + 'static,{ client::ClientRequest::post(url) .form(params).into_future().and_then(|req| { req.send() .map_err(Error::from).and_then(|resp| { if resp.status().is_success() { let fut = resp.json::<O>().from_err(); boxed(fut) } else { error!("Microservice error: {}", resp.status ()); let fut = Err(format_err!("microservice error")) .into_future().from_err(); boxed(fut) } }) })}
post_request函数使用ClientRequest的post方法创建ClientRequestBuilder,并使用params变量中的值填充表单。 我们将Result转换为Future并将请求发送到服务器。 此外,与GET版本一样,我们处理响应,但是以另一种方式执行。 我们通过HttpResponse的状态方法调用获得响应状态,并使用is_sucess方法调用检查它是否成功。
为了成功响应,我们使用HttpResponse的json方法来获取一个收集正文并从JSON反序列化它的Future。 如果响应不成功,我们会向客户端返回错误。 现在,我们有方法将请求发送到其他微服务,并且可以为每个路由实现处理程序。
处理程序
我们添加了代理传入请求的方法,并将它们重新发送到其他微服务。 现在,我们可以为微服务的每个支持路径实现处理程序,我们将为客户端提供整体API,但实际上,我们将使用一组微服务为客户端提供所有必要的服务。 让我们从/ signup路径的处理程序的实现开始。
注册
路由器微服务使用/ signup路由向注册到127.0.0.1:8001地址的用户微服务重新发送注册请求。此请求创建一个从UserForm填充的新用户,使用Form类型包装的参数传递。看下面的代码:
fn signup(params: Form<UserForm>) -> FutureResponse<HttpResponse> { let fut = post_request("http://127.0.0.1:8001/signup", params.into_inner()) .map(|_: ()| { HttpResponse::Found() .header(header::LOCATION, "/login.html") .finish() }); Box::new(fut)}
我们调用之前声明的post_request函数向用户微服务发送POST请求,如果它返回成功的响应,我们返回一个带有302状态代码的响应。我们通过HttpResponse :: Found函数调用创建带有相应状态代码的HttpResponseBuilder。在此之后,我们还设置了LOCATION标头,以使用HttpResponseBuilder的头方法调用将用户重定向到登录表单。最后,我们调用finish()从构建器创建HttpResponse并将其作为盒装的Future对象返回。该函数具有FutureResponse返回类型,其实现如下:
type FutureResponse<I, E = Error> = Box<dyn Future<Item = I, Error = E>>;
如您所见,它是一个具有实现Future特征的类型的Box。此外,该函数需要Form 作为参数。 UserForm结构声明如下:
#[derive(Deserialize, Serialize)]pub struct UserForm { email: String, password: String,}
如您所见,它需要两个参数:电子邮件和密码。两者都将从请求的查询字符串中提取,格式为email=user@example.com&passwo d = 。表单包装器有助于从响应主体中提取数据。
actix_web crate按大小限制请求和响应。 如果要发送或接收大量有效负载,则必须覆盖通常不允许大于256 KB的请求的默认值。 例如,如果要增加限制,可以使用随Route的with_config方法调用提供的FormConfig结构,并使用所需的字节数调用config的limit方法。 HTTP客户端也受响应大小的限制。 例如,如果您尝试从JsonBody实例读取大型JSON对象,则在将其用作Future对象之前,可能需要使用limit方法调用来限制它。
登录
其他方法允许用户使用提供的凭据登录微服务。 查看以下signin函数,该函数处理发送到/ signin路径的请求:
fn signin((req, params): (HttpRequest<State>, Form<UserForm>)) -> FutureResponse<HttpResponse>{ let fut = post_request("http://127.0.0.1:8001/signin", params.into_inner()) .map(move |id: UserId| { req.remember(id.id); HttpResponse::build_from(&req) .status(StatusCode::FOUND) .header(header::LOCATION, "/comments.html") .finish() }); Box::new(fut)}
该函数有两个参数:HttpRequest和Form。 首先我们需要访问共享的State对象。 第二,我们需要从请求体中提取UserForm结构。 我们也可以在这里使用post_request函数,但希望它在响应中返回UserId值。 UserId结构声明如下:
#[derive(Deserialize)]
pub struct UserId { id: String,}
由于HttpRequest实现了RequestIdentity特性并且我们将IdentityService插入App,因此我们可以使用用户ID调用remember方法将当前会话与用户相关联。
在此之后,我们使用302状态代码创建响应,就像我们在上一个处理程序中所做的那样,并将用户重定向到/comments.html页面。 但是我们必须从HttpRequest构建一个HttpResponse实例来保持remember函数调用的更改。
新评论
用于创建新注释的处理程序使用用户的标识来检查是否存在用于添加新注释的凭据:
fn new_comment((req, params): (HttpRequest<State>, Form<AddComment>)) -> FutureResponse<HttpResponse>{ let fut = req.identity() .ok_or(format_err!("not authorized").into()) .into_future() .and_then(move |uid| { let params = NewComment { uid, text: params.into_inner().text, }; post_request::<_, ()>("http://127.0.0.1:8003/new_comment", params) }) .then(move |_| { let res = HttpResponse::build_from(&req) .status(StatusCode::FOUND) .header(header::LOCATION, "/comments.html") .finish(); Ok(res) }); Box::new(fut)}
此处理程序允许已签名的每个用户发表评论。 我们来看看这个处理程序是如何工作的。
首先,它调用RequestIdentity特征的identity方法,该方法返回用户的ID。 我们将其转换为Result,以便将其转换为Future,如果未识别用户则返回错误。
我们使用返回的用户ID值来准备注释微服务的请求。 我们从AddComment表单中提取文本字段,并使用用户的ID和注释创建一个NewComment结构。 结构声明如下:
#[derive(Deserialize)]
pub struct AddComment { pub text: String,}#[derive(Serialize)]
我们也可以使用带有可选uid的单个结构,但是为不同的需求使用单独的结构更安全,因为如果我们使用相同的结构并将其重新发送到另一个微服务而不进行验证,我们可以创建一个漏洞,允许任何用户 添加其他用户身份的评论。 尝试通过使用精确,严格的类型而不是通用的,灵活的类型来避免这些错误。
最后,我们像之前一样创建客户端重定向,并将用户发送到/comments.html页面。
评论
要查看前一个处理程序创建的所有注释,我们必须使用之前创建的get_request函数向注释微服务发送GET请求,并将响应数据重新发送到客户端:
fn comments(_req: HttpRequest<State>) -> FutureResponse<HttpResponse> { let fut = get_request("http://127.0.0.1:8003/list") .map(|data| { HttpResponse::Ok().body(data) }); Box::new(fut)}
计数器
打印请求总数的处理程序也具有非常简单的实现,但在这种情况下,我们可以访问共享状态:
fn counter(req: HttpRequest<State>) -> String { format!("{}", req.state().0.borrow())}
我们使用HttpRequest的state方法来获取对State实例的引用。 由于计数器值存储在RefCell中,我们使用借用方法从单元格中获取值。 我们实现了所有处理程序,现在我们必须添加一些中间件来计算对微服务的每个请求。
中间件
actix-web包支持可以附加到App实例以处理每个请求和响应的中间件。 中间件有助于记录请求,转换请求,甚至使用正则表达式控制对一组路径的访问。 将中间件视为所有传入请求和传出响应的处理程序。 要创建中间件,我们首先必须为它实现中间件特性。 看下面的代码:
pub struct Counter;impl Middleware<State> for Counter { fn start(&self, req: &HttpRequest<State>) -> Result<Started> { let value = *req.state().0.borrow(); *req.state().0.borrow_mut() = value + 1; Ok(Started::Done) } fn response(&self, _req: &HttpRequest<State>, resp: HttpResponse) -> Result<Response> { Ok(Response::Done(resp))
} fn finish(&self, _req: &HttpRequest<State>, _resp: &HttpResponse) -> Finished { Finished::Done }}
我们声明一个空的Counter结构并为它实现Middleware特性。
中间件特征具有带状态的类型参数。 由于我们想要使用State结构的计数器,我们将其设置为类型参数,但是如果要创建与不同状态兼容的中间件,则需要向实现添加类型参数并添加必要特征的实现 您可以导出到您的模块或板条箱。
中间件特征包含三种方法。 我们实施了所有这些:
- 当请求就绪时调用start,并将其发送给处理程序
- 在处理程序返回响应后调用response
- 在将数据发送到客户端时调用finish
我们使用响应和完成方法的默认实现。
对于第一种方法,我们在Response :: Done包装器中返回一个没有任何更改的响应。如果你想返回一个生成HttpResponse的Future,Response也有一个变体Future。
对于第二种方法,我们返回Finished枚举的Done变体。它还有一个Future变体,它可以包含一个盒装的Future对象,它将在finish方法结束后运行。让我们探讨一下start方法在我们的实现中是如何工作的。
在Counter中间件的start方法实现中,我们将计算所有传入的请求。为此,我们从RefCell获取当前计数器值,添加1,并用新值替换单元格。最后,该方法返回一个Started :: Done值,通知您当前请求将在处理链的下一个处理程序/中间件中重用。 Started枚举也有变种:
- 如果要立即返回响应,则应使用响应
- 如果您想运行将返回响应的Future,则应使用Future
现在,微服务已经准备好构建和运行。
建立和运行
要运行微服务,请使用cargo run命令。 由于我们没有处理程序的其他微服务,我们可以使用计数器方法来检查服务器和Counter中间件是否正常工作。 尝试在浏览器中打开http://127.0.0.1:8080/stats/counter。 它将在空白页面上显示1值。 如果刷新页面,您将看到3值。 那是因为浏览器还在主要请求之后发送获取favicon.ico文件的请求。
使用数据库
actix-web的另一个优点是与actix crate结合使用,是使用数据库的能力。 你还记得我们是如何使用SyncArbyter来执行后台任务的吗? 这是实现数据库交互的好方法,因为没有足够的异步数据库连接器,我们必须使用同步连接器。 让我们为前面的例子添加对Redis数据库的响应缓存。
数据库交互actor
我们首先实现一个与数据库交互的actor。 复制上一个示例并将redis crate添加到依赖项:
redis = "0.9"
我们使用Redis是因为它非常适合缓存,但我们也可以将缓存值存储在内存中。
创建一个src / cache.rs模块并添加以下依赖项:
use actix::prelude::*;use failure::Error;use futures::Future;use redis::{Commands, Client, RedisError};
它从我们在第7章可靠集成数据库中使用的redis crate添加类型,以与Redis存储进行交互。
Actors
我们的演员必须保留客户端的实例。 我们不使用连接池,因为我们将使用多个actor将并行请求传递给数据库。 查看以下结构:
pub struct CacheActor { client: Client, expiration: usize,}
该结构还包含一个保留生存时间(TTL)周期的到期字段。 这定义了Redis保存该值的时间。 向使用提供的地址字符串创建Client实例的实现添加新方法,并将客户端和到期值添加到CacheActor结构中,如下所示:
impl CacheActor { pub fn new(addr: &str, expiration: usize) -> Self { let client = Client::open(addr).unwrap(); Self { client, expiration } }}
此外,我们必须为SyncContext实现Actor特性,就像我们在第10章,后台任务和微服务中的线程池中调整工作者大小时所做的那样:
impl Actor for CacheActor { type Context = SyncContext<Self>;}
现在,我们可以添加对消息的支持以设置和获取缓存值。
消息(Messages)
要与CacheActor进行交互,我们必须添加两种类型的消息:设置值并获取值。
设置值消息
我们添加的第一个消息类型是SetValue,它为缓存提供了一对密钥和新值。 该结构有两个字段路径,用作键,以及内容,它包含一个值:
struct SetValue { pub path: String, pub content: Vec<u8>,}
如果设置了值,让我们为SetValue结构实现一个Message trait,并使用空单元类型,如果数据库连接有问题,则返回RedisError:
impl Message for SetValue { type Result = Result<(), RedisError>;}
CacheActor支持接收SetValue消息。 让我们用Handler特性来实现它:
impl Handler<SetValue> for CacheActor { type Result = Result<(), RedisError>; fn handle(&mut self, msg: SetValue, _: &mut Self::Context) -> Self::Result { self.client.set_ex(msg.path, msg.content, self.expiration) }}
我们使用存储在CacheActor中的Client实例,通过set_ex方法调用从Redis执行SETEX命令。 此命令设置一个有效期限的值,以秒为单位。 如您所见,该实现与第7章可靠集成数据库的数据库交互功能非常接近,但实现为特定消息的处理程序。 这种代码结构更简单,更直观。
获取价值信息
GetValue结构表示一个消息,用于通过键(或路径,在我们的示例中)从Redis中提取值。 它只包含一个具有路径值的字段:
struct GetValue { pub path: String,}
我们还必须为它实现Message特性,但是如果Redis包含提供的键的值,我们希望它返回一个可选的Vec 值:
impl Message for GetValue { type Result = Result<Option<Vec<u8>>, RedisError>;}
CacheActor还为GetValue消息类型实现Handler特征,并通过调用Client的get方法从存储中提取值来使用Redis存储的GET命令:
impl Handler<GetValue> for CacheActor { type Result = Result<Option<Vec<u8>>, RedisError>; fn handle(&mut self, msg: GetValue, _: &mut Self::Context) -> Self::Result { self.client.get(&msg.path) }}
如您所见,演员和消息很简单,但我们必须使用Addr值与它们进行交互。 这不是一个简洁的方法。 我们将添加一个允许方法与CacheActor实例交互的特殊类型。
链接到actor
以下结构包装CacheActor的地址:
#[derive(Clone)]pub struct CacheLink { addr: Addr<CacheActor>,}
构造函数仅使用Addr值填充此addr字段:
impl CacheLink { pub fn new(addr: Addr<CacheActor>) -> Self { Self { addr } }}
我们需要一个CacheLink包装结构来添加访问缓存功能的方法,但需要隐藏实现细节和消息交换。 首先,我们需要一种方法来获取缓存值:
pub fn get_value(&self, path: &str) -> Box<Future<Item = Option<Vec<u8>>, Error = Error>> { let msg = GetValue { path: path.to_owned(), }; let fut = self.addr.send(msg) .from_err::<Error>() .and_then(|x| x.map_err(Error::from)); Box::new(fut)}
上述函数创建一个带有路径的新GetValue消息,并将此消息发送到Cacher中包含的Addr。 在此之后,它等待结果。 该函数将此交互序列作为盒装Future返回。
下一个方法以类似的方式实现–set_value方法通过向CacheActor发送SetValue消息来为缓存设置新值:
pub fn set_value(&self, path: &str, value: &[u8]) -> Box<Future<Item = (), Error = Error>> { let msg = SetValue { path: path.to_owned(), content: value.to_owned(), }; let fut = self.addr.send(msg) .from_err::<Error>() .and_then(|x| x.map_err(Error::from)); Box::new(fut)}
为了编写消息,我们使用路径和字节数组引用转换为Vec 值。 现在,我们可以在服务器实现中使用CacheActor和CacheLink。
使用数据库actor
在本章的前一个示例中,我们使用共享状态来提供对存储为i64的计数器的访问,该计数器包含在RefCell中。我们重用此结构,但添加CacheLink字段以使用与CacheActor的连接来获取或设置缓存值。添加此字段:
struct State { counter: RefCell<i64>, cache: CacheLink,}
我们之前为State结构派生了一个Default trait,但现在我们需要一个新的构造函数,因为我们必须提供一个CacheLink实例,其中包含缓存actor的实际地址:
impl State { fn new(cache: CacheLink) -> Self { Self { counter: RefCell::default(), cache, } }}
在大多数情况下,缓存以这种方式工作 - 它尝试从缓存中提取值;如果它存在且尚未过期,则将值返回给客户端。如果没有有效值,我们需要获得一个新值。完成之后,我们必须将其存储在缓存中以备将来使用。
在前面的示例中,我们经常使用Future实例从另一个微服务接收响应。为了简化我们对缓存的使用,让我们将缓存方法添加到State实现中。此方法将使用路径包装任何提供的future,并尝试提取缓存的值。如果该值不可用,它将获得一个新值,然后,它将接收存储复制的值到缓存,并将值返回给客户端。此方法将提供的Future值与另一个Future trait实现包装在一起。请看以下实现:
fn cache<F>(&self, path: &str, fut: F) -> impl Future<Item = Vec<u8>, Error = Error>where F: Future<Item = Vec<u8>, Error = Error> + 'static,{ let link = self.cache.clone(); let path = path.to_owned(); link.get_value(&path) .from_err::<Error>() .and_then(move |opt| { if let Some(cached) = opt { debug!("Cached value used"); boxed(future::ok(cached)) } else { let res = fut.and_then(move |data| { link.set_value(&path, &data) .then(move |_| { debug!("Cache updated"); future::ok::<_, Error>(data) }) .from_err::<Error>() }); boxed(res) } })}
该实现使用State实例来克隆CacheLink。我们必须使用克隆链接,因为我们必须将它移动到使用它来存储新值的闭包,如果我们需要获取它。
首先,我们调用CacheLink的get_value方法并获取一个从缓存中请求值的Future。由于该方法返回Option,我们将使用and_then方法检查缓存中是否存在该值,并将该值返回给客户端。如果值已过期或不可用,我们将通过执行提供的Future获取它,并使用链接调用set_value方法(如果成功返回新值)。
现在,我们可以使用cache方法来缓存为前一个示例的comments处理程序返回的注释列表:
fn comments(req: HttpRequest<State>) -> FutureResponse<HttpResponse> { let fut = get_request("http://127.0.0.1:8003/list"); let fut = req.state().cache("/list", fut) .map(|data| { HttpResponse::Ok().body(data) }); Box::new(fut)}
首先,我们使用之前实现的get_request方法创建Future以从另一个微服务获取值。之后,我们使用请求的state方法获取State的引用,并通过传递/ list路径调用cache方法,然后创建Future实例以获取新值。
我们已经实现了数据库actor的所有部分。我们仍然需要使用SyncArbiter启动一组缓存actor,并使用CacheLink包装返回的Addr值:
let addr = SyncArbiter::start(3, || { CacheActor::new("redis://127.0.0.1:6379/", 10)});let cache = CacheLink::new(addr);server::new(move || { let state = State::new(cache.clone()); App::with_state(state) // remains part App building})
现在,您可以构建服务器。它将每10秒返回/ api / list请求的缓存值。
使用actor的另一个好处是WebSocket。有了这个,我们可以使用作为actor实现的状态机为我们的微服务添加有状态交互。我们在下一节中看一下。