RabbitMQ Spring-AMQP官方教程(六)--RPC

Remote procedure call (RPC,远程过程调用)

In the second tutorial we learned how to use Work Queues to distribute time-consuming tasks among multiple workers.

在教程2里我们学习了如何使用工作队列在多个工作者之间分发耗时任务。

But what if we need to run a function on a remote computer and wait for the result? Well, that's a different story. This pattern is commonly known as Remote Procedure Call or RPC.

但如果我们需要在一个远程电脑上运行一个函数并且等待运行结果的话要怎么办呢?这就变成另一个问题了。这种模式通常被称为远程过程调用(Remote Procedure Call),或者简称RPC。

In this tutorial we're going to use RabbitMQ to build an RPC system: a client and a scalable RPC server. As we don't have any time-consuming tasks that are worth distributing, we're going to create a dummy RPC service that returns Fibonacci numbers.

在本节教程里,我们将用RabbitMQ来构建一个RPC系统,这个系统包括一个客户端和一个可伸缩的RPC服务端。由于我们没有什么耗时任务值得分发,所以我们准备创建一个假的RPC服务,这个服务返回斐波那契(Fibonacci)数值。

Client interface(客户端接口)

To illustrate how an RPC service could be used we're going to change the names of our profiles from "Sender" and "Receiver” to "Client" and "Server". When we call the server we will get back the fibonacci of the argument we call with.

为了说明RPC服务可以如何被使用,我们准备修改我们的配置组,将名称从“Sender”和“Receiver”换成“Client”和“Server”。当我们调用服务端时,我们将会获得我们传入的参数所对应的斐波那契数值。

Integer response = (Integer) template.convertSendAndReceive
(exchange.getName(), "rpc", start++);
System.out.println(" [.] Got '" + response + "'");

A note on RPC(RPC的注意点)

Although RPC is a pretty common pattern in computing, it's often criticised. The problems arise when a programmer is not aware whether a function call is local or if it's a slow RPC. Confusions like that result in an unpredictable system and adds unnecessary complexity to debugging. Instead of simplifying software, misused RPC can result in unmaintainable spaghetti code.

虽然RPC在计算领域是很常见的模式,但它通常也是受争议的。但程序员不知道一个函数调用是本地的还是慢速的RPC时就会出现一些问题。像这样的混乱会导致不可预知的系统,而且会给调试增加不必要的复杂性。不恰当地使用RPC不仅不会简化程序,还会导致代码变得很难维护。

Bearing that in mind, consider the following advice:

记住这一点,然后考虑一下几点建议:

  • Make sure it's obvious which function call is local and which is remote.(确保哪个函数调用是本地的,哪个是远程的。)
  • Document your system. Make the dependencies between components clear.(为你的系统做好文档。清晰化组件间的依赖。)
  • Handle error cases. How should the client react when the RPC server is down for a long time?(处理好会发生错误的场景。但RPC服务端长时间挂掉时,客户端应该做出什么反应?)

When in doubt avoid RPC. If you can, you should use an asynchronous pipeline - instead of RPC-like blocking, results are asynchronously pushed to a next computation stage.

当你无法对这些问题无法做出明确回答时,就不要使用RPC。如果可以的话,你应该使用异步pipeline,而不是类似于阻塞的RPC。使用异步pipeline,计算结果可以异步推入到下一个计算阶段。

Callback queue(回调队列)

In general doing RPC over RabbitMQ is easy. A client sends a request message and a server replies with a response message. In order to receive a response we need to send a 'callback' queue address with the request. Spring-amqp's RabbitTemplate handles the callback queue for us when we use the above 'convertSendAndReceive()' method. There is no need to do any other setup when using the RabbitTemplate. For a thorough explanation please see Request/Reply Message.

一般情况下,在RabbitMQ上实现RPC挺简单的。客户端发送请求消息然后服务端返回一个响应消息。为了接收响应消息,我们必须传送一个用于处理请求的回调队列。在我们使用“convertSendAndReceive()”方法时,Spring-amqp框架的RabbitTemplate类为我们做好了回调队列的处理工作。使用RabbitTemplate类时无需在做其它配置。若想看完整的文档,请参阅请求/发送消息

Message properties(消息属性)

The AMQP 0-9-1 protocol predefines a set of 14 properties that go with a message. Most of the properties are rarely used, with the exception of the following:

AMQP 0-9-1协议预定义了14个消息属性。大部分的属性都很少用到,除了以下几个:

  • deliveryMode: Marks a message as persistent (with a value of 2) or transient (any other value). You may remember this property from the second tutorial.
  • deliveryMode:将消息标记为要持久化(此时属性值为2)或者瞬态(此时属性值为2以外的其它数字)。教程2里提到过这
    个属性,你应该还记得。
  • contentType: Used to describe the mime-type of the encoding. For example for the often used JSON encoding it is a good practice to set this property to: application/json.
  • contentType:用来描述编码的mime类型。例如,对于常用的JSON格式,最好将这个属性值设为application/json。
  • replyTo: Commonly used to name a callback queue.
  • replayTo:通常用来命名一个回调队列。
  • correlationId: Useful to correlate RPC responses with requests.
  • correlationId:该属性用来将RPC响应与请求进行关联。

Correlation Id(关联Id)

Spring-amqp allows you to focus on the message style you're working with and hide the details of message plumbing required to support this style. For example, typically the native client would create a callback queue for every RPC request. That's pretty inefficient so an alternative is to create a single callback queue per client.

Spring-amqp能让你专注于正在处理的消息类型,并隐藏了支持该类型的消息所需的消息管道的实现细节。例如,通常情况下,本地客户端会为每个RPC请求都创建一个回调队列。这种做法效率很低,所以替换方案是每个客户端只创建一个回调队列。

That raises a new issue, having received a response in that queue it's not clear to which request the response belongs. That's when the correlationId property is used. Spring-amqp automatically sets a unique value for every request. In addition it handles the details of matching the response with the correct correlationId.

但这会导致一个新的问题,那就是,对于从这个队列里接收的响应,我们无法知道它对应的是哪个请求。这时候,correlationId就派上用场了。spring-amqp自动帮我们为每一个请求设好了唯一的correlationId值。而且,它还帮我们做好了将响应与correlationId进行匹配的细节。

One reason that spring-amqp makes rpc style easier is that sometimes you may want to ignore unknown messages in the callback queue, rather than failing with an error. It's due to a possibility of a race condition on the server side. Although unlikely, it is possible that the RPC server will die just after sending us the answer, but before sending an acknowledgment message for the request. If that happens, the restarted RPC server will process the request again. The spring-amqp client handles the duplicate responses gracefully, and the RPC should ideally be idempotent.

spring-amqp使得rpc模式变得简单的一个原因是,有时你可能会想忽略回调队列里的一些未知消息,而不是抛出错误。这是因为服务端可能会出现竞争的情况。有可能RPC服务端在给我们发送完响应但却还没来得及发送确认消息时,它就挂了,虽然看起来不大像会这样。如果发生了这种情况,重启RPC服务端会继续再去处理这条请求。spring-amqp客户端会优雅地处理重复的响应,这种情况下,RPC应该是完美幂等的。

Summary(总结)

image

Our RPC will work like this:

我们的RPC系统

1.The Tut6Config will setup a new DirectExchange and a client

在Tut6Config文件里将建立一个新的DirectExchange和一个客户端。

2.The client will leverage the convertSendAndReceive passing the exchange name, the routingKey, and the message.

客户端将使用convertSendAndReceive,并传入交换器名字,路由键和消息。

3.The request is sent to an rpc_queue("tut.rpc") queue.

请求被发送到用于rpc的队列里(“tut.rpc”)。

4.The RPC worker (aka: server) is waiting for requests on that queue. When a request appears, it performs the task and sends a message with the result back to the Client, using the queue from the replyTo field.

RPC工作者(也就是服务器)等待发送到队列里的请求。但一个请求出现时,它就执行任务,然后通过使用replyTo域里配置的队列将带有结果的消息发回给客户端。

5.The client waits for data on the callback queue. When a message appears, it checks the correlationId property. If it matches the value from the request it returns the response to the application. Again, this is done automagically via the RabbitTemplate.

客户端等待回调队列里的数据。当一条消息出现时,它会校验correlationId属性。如果属性值与请求匹配,它就将响应返回给应用。这个工作RabbitTemplate自动帮我们完成了。

Putting it all together(代码整合)

The Fibonacci task is a @RabbitListener and is defined as:

计算斐波那契的任务用@RabbitListener进行标注,任务内容的定义如下:

public int fib(int n) {
    return n == 0 ? 0 : n == 1 ? 1 : (fib(n - 1) + fib(n - 2));
}

We declare our fibonacci function. It assumes only valid positive integer input. (Don't expect this one to work for big numbers, and it's probably the slowest recursive implementation possible).

我们声明了斐波那契函数。它假定输入的参数是有效的正整数。(不要期望它能用于大数的场景,而且这种方式是最低效的递归实现)。

The code for our Tut6Config looks like this:

Tut6Config的代码看起来是如下这样子的:

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Profile({"tut6","rpc"})
@Configuration
public class Tut6Config {

    @Profile("client")
    private static class ClientConfig {
    
        @Bean
        public DirectExchange exchange() {
            return new DirectExchange("tut.rpc");
        }
        
        @Bean
        public Tut6Client client() {
            return new Tut6Client();
        }
    
    }
    
    @Profile("server")
    private static class ServerConfig {
    
        @Bean
        public Queue queue() {
            return new Queue("tut.rpc.requests");
        }
        
        @Bean
        public DirectExchange exchange() {
            return new DirectExchange("tut.rpc");
        }
        
        @Bean
        public Binding binding(DirectExchange exchange, Queue queue) {
            return BindingBuilder.bind(queue).to(exchange).with("rpc");
        }
        
        @Bean
        public Tut6Server server() {
            return new Tut6Server();
        }
    
    }
}

It setups up our profiles as "tut6" or "rpc". It also setups a "client" profile with two beans; 1) the DirectExchange we are using and 2) the Tut6Client itself. We also configure the "server" profile with three beans, the "tut.rpc.requests" queue, the DirectExchange, which matches the client's exchange, and the binding from the queue to the exchange with the "rpc" routing-key.

它建立了我们的配置组,叫“tut6”或者“rpc”。同时,还建立了一个“client”配置组,这个组里配置了两个bean:一个是我们将要用到的DirectExchange类型的交换器,一个是Tut6Client本身。我们还建立了一个“server”配置组,这个组里配置了三个bean:一个名为“tut.rpc.requests”的队列,一个与客户端交换器相匹配的DirectExchange类型的交换器,以及用名为“rpc”的路由键将队列和交换器的绑定器。

The server code is rather straightforward:

服务端代码更直观点:

1.As usual we start annotating our receiver method with a @RabbitListener and defining the queue its listening on.

像之前那样,我们先用@RabbitListener来注解我们的接收者方法,然后定义它要监听的队列。

2.Our fibanacci method calls fib() with the payload parameter and returns the result.

我们的斐波那契方法被命名为fib(),接收有效参数并返回结果。

The code for our RPC server Tut6Server.java:

以下为我们的RPC服务端代码Tut6Server.java:

package org.springframework.amqp.tutorials.tut6;

import org.springframework.amqp.rabbit.annotation.RabbitListener;

public class Tut6Server {

    @RabbitListener(queues = "tut.rpc.requests")
    // @SendTo("tut.rpc.replies") used when the
    // client doesn't set replyTo.
    public int fibonacci(int n) {
        System.out.println(" [x] Received request for " + n);
        int result = fib(n);
        System.out.println(" [.] Returned " + result);
        return result;
    }
    
    public int fib(int n) {
        return n == 0 ? 0 : n == 1 ? 1 : (fib(n - 1) + fib(n - 2));
    }

}

The client code Tut6Client is as easy as the server:

客户端代码Tut6Client与服务端代码一样简单:

1.We autowire the RabbitTemplate and the DirectExchange bean as defined in the Tut6Config.

我们自动注入Tut6Config里定义的类型为RabbitTemplate和DirectExchange的bean。

2.We invoke template.convertSendAndReceive with the parameters exchange name, routing key and message.

我们调用template.convertSendAndReceive,传入的参数为交换器名字,路由键以及消息。

3.We print the result.

打印出结果。

Making the Client request is simply:

发起客户端请求也很简单:

import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

public class Tut6Client {

    @Autowired
    private RabbitTemplate template;
    
    @Autowired
    private DirectExchange exchange;
    
    int start = 0;
    
    @Scheduled(fixedDelay = 1000, initialDelay = 500)
    public void send() {
        System.out.println(" [x] Requesting fib(" + start + ")");
        Integer response = (Integer) template.convertSendAndReceive(exchange.getName(), "rpc", start++);
        System.out.println(" [.] Got '" + response + "'");
    }
}

Using the project setup as defined in (see tutorial one) with start.spring.io and SpringInitialzr the preparing the runtime is the same as the other tutorials:

mvn clean package

We can start the server with:

我们可以用以下命令来启动服务端:

java -jar target/rabbit-tutorials-1.7.1.RELEASE.jar
--spring.profiles.active=rpc,server
--tutorial.client.duration=6000

To request a fibonacci number run the client:

若要请求斐波那契数值,可以用以下命令来启动客户端:

java -jar target/rabbit-tutorials-1.7.1.RELEASE.jar
--spring.profiles.active=rpc,server
java -jar target/rabbit-tutorials-1.7.1.RELEASE.jar
--spring.profiles.active=rpc,client

The design presented here is not the only possible implementation of a RPC service, but it has some important advantages:

以上展示的设计不仅仅是实现RPC服务的一种方式,而且还有一些重要的优点:

1.If the RPC server is too slow, you can scale up by just running another one. Try running a second RPC Server in a new console.

如果RPC服务端太慢,你可以仅仅通过额外再运行一个服务端来增大其规模。可以尝试着在新的控制台里运行第二个RPC服务端。

2.On the client side, the RPC requires sending and receiving only one message with one method. No synchronous calls like queueDeclare are required. As a result the RPC client needs only one network round trip for a single RPC request.

在客户端这一边,RPC要求只能一个方法里发送和接收一条消息。不需要像queueDelare那样的同步调用。于是,对于一个RPC请求,RPC客户端只需一个网络回路。

Our code is still pretty simplistic and doesn't try to solve more complex (but important) problems, like:

我们的代码仍然很简单,而且没有处理很多复杂(但重要)的问题,像:

1.How should the client react if there are no servers running?

如果没有服务端正在运行,客户端应该做出什么反应?

2.Should a client have some kind of timeout for the RPC?

客户端需要为RPC做超时处理吗?

3.If the server malfunctions and raises an exception, should it be forwarded to the client?

如果服务端出现故障并引起了异常,它是否应该告知客户端?

4.Protecting against invalid incoming messages (eg checking bounds, type) before processing.

在处理进来的消息之前先处理掉无效的消息(如校验边界,类型)。

If you want to experiment, you may find the management UI useful for viewing the queues.

如果你想做些实验,可以通过管理界面来查看队列,你会发现它很有用的。

There is one other nice feature of RabbitMQ. It is featured as a supported tile on Pivotal Cloud Foundry (PCF) as a service.

RabbitMQ还有另一个很棒的特性。它作为服务在Pivotal Cloud Foundry(PCF)上被支持。

转自我的博客:https://jiapengcai.github.io/posts/40514/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,106评论 0 10
  • 你死在回家的路上。 是车祸。没有很离奇,但 依旧致命。你丢下妻子和一双子女。你死得并不痛苦。急救人员尽了最大努力,...
    无零零阅读 557评论 3 2
  • 感儿子陪我度过赏快乐每一天
    味道hxh阅读 105评论 0 0
  • 1、kotlin的lamda语法有很多,特别是foreach语法: 众多的语法中是不是选取自己喜欢的语法就好了呢?...
    stormKid阅读 566评论 0 2