精读分布式系统核心:面向服务的分布式架构,消息通信常用模式

消息通信常用模式

消息中间件不管是在企业级应用中还是在互联网产品中,其应用的场景非常广泛。本节以RabbitMQ为例,总结消息通信常用模式。

工作队列

工作队列(Work Queues)又叫作任务队列(Task Queues),背后主要的思想是避免立即处理一个资源密集型任务所造成的长时间等待,相反我们可以计划着让任务后续执行。我们将任务封装成消息发送到队列中,一个worker(工作者)进程在后台运行,获取任务并最终执行任务。当运行多个worker时,所有的任务将会被它们所共享。

下图所示是工作队列的示意图。

精读分布式系统核心:面向服务的分布式架构,消息通信常用模式图7-3 工作队列示意图

下面是一个工作队列的代码示例。

public class NewTask {
private static final Logger LOGGER = LoggerFactory.getLogger(NewTask.class);
private static final String TASK_QUEUE_NAME = "waylau_queue";
public static void main(String[] argv) throws Exception {
// 初始化连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置连接的地址
factory.setHost("localhost");// 获得连接
Connection connection = factory.newConnection();
// 创建 Channel
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
// 从控制台参数中,获取消息
String message = getMessage(argv);
// 发送消息,发送10条
for (int i = 0; i < 10; i++) {
String msg = message + " " + i;
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.
PERSISTENT_TEXT_PLAIN,
(msg).getBytes("UTF-8"));
LOGGER.info(" [x] Sent '" + msg + "'");
}
channel.close();
connection.close();
}
...
}

发布/订阅

发布/订阅(Publish/Subscribe)在消息队列中是一种比较常见的工作模式,如图7-4所示。该模式定义了如何向一个内容节点发布和订阅消息,内容节点也叫主题,主题是为发布者(Publisher)和订阅者(Subscriber)提供传输的中介。发布/订阅模型使发布者和订阅者之间不需要直接通信(如RMI)就可保证消息的传送,有效解决了系统间耦合问题。该模式同时也定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。

精读分布式系统核心:面向服务的分布式架构,消息通信常用模式图7-4 发布/订阅示意图

在该模式中,producer(生产者)并不直接发送消息到queue(队列),而是发到了exchange(交换器)中。exchange一边接收来自producer的消息,另外一边将消息插入queue。在上一个例子中,我们并没有显式地使用exchange,我们仍然可以发送和接收消息。这是因为我们使用了一个默认的转发器,它的标识符为""。之前发送消息的代码如下。

channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties. PERSISTENT_TEXT_PLAIN,
(msg).getBytes("UTF-8"));

这种没有显式命名的exchange被称为nameless exchange(无名交换器)。

exchange必须清楚接收到消息的下一步用途,比如,是要将消息插入一个queue中还是插入多个queue中。exchange type(交换器类型)标识的消息的用途,主要有direct、topic、headers和fanout。比如,我们要创建一个fanout类型的exchange,可以使用以下方法。

channel.exchangeDeclare("logs", "fanout");

fanout类型的exchange特别简单——把所有它接收到的消息广播到所有它知道的队列。

下面是一个发布/订阅的代码示例。

public class EmitLog {
private static final Logger LOGGER = LoggerFactory.getLogger(EmitLog. class);
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明交换器和类型
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = getMessage(argv);
// 往交换器上发送消息
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
LOGGER.info(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
...
}

路由

路由(Routing)意味在消息订阅中,可选择性地只订阅部分消息,如图7-5所示。比如,在日志系统中,我们可以只接收Error级别的消息写入文件。同时仍然可以在控制台输出所有日志。

精读分布式系统核心:面向服务的分布式架构,消息通信常用模式图7-5 路由示意图

在本例中,我们将使用direct类型的exchange,这样消息会被推送至binding key(绑定键)和消息发布附带的routing key(路由键)完全匹配的队列。比如:

channel.queueBind(queueName, EXCHANGE_NAME, "error");

其中的第3个参数error就是routing key。

下面是一个路由的代码示例。

public class EmitLogDirect {
private static final Logger LOGGER = LoggerFactory.getLogger
(EmitLogDirect.class);
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String severity = getSeverity(argv);
String message = getMessage(argv);
// 为简化程序,这里的severity是inof、warning、error中的一个
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes ("UTF-8"));
LOGGER.info(" [x] Sent '" + severity + "':'" + message + "'");
channel.close();
connection.close();
}
...
}

主题

主题类型的exchange拥有比direct类型更多的灵活性。topic exchange的routing key可以是长度不超过255字节的字符,其格式是以点号“。”进行分割的,比如stock.usd.nyse、nyse.vmw或者quick.orange.rabbit。需要注意的是,所绑定的key必须是相同的格式。其中,有两个特殊的字符。

·*代表任意一个单词。

·#代表0个或者多个单词。例如,某个routing key的格式为..。其中speed代表速度,color代表颜色,species代表种类。那么*.orange.*代表了所有颜色是orange的动物,lazy.#代表了所有懒惰的动物。

主题示意如下图所示。

精读分布式系统核心:面向服务的分布式架构,消息通信常用模式图7-6 主题示意图

下面是一个主题的代码示例。

public class ReceiveLogsTopic {
private static final Logger LOGGER = LoggerFactory.getLogger
(ReceiveLogsTopic.class);
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 声明一个Topic类型的exchange
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String queueName = channel.queueDeclare().getQueue();
if (argv.length < 1) {
LOGGER.error("Usage: ReceiveLogsTopic[binding_key]...");
System.exit(1);
}
for (String bindingKey : argv) {
channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
}
LOGGER.info(" [*] Waiting for messages. To exit press CTRL+C");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
LOGGER.info(" [x] Received '" + envelope.getRoutingKey() + "':'" +
message + "'");
}};
channel.basicConsume(queueName, true, consumer);
}
}

RPC

本例演示了如何在RabbitMQ里实现RPC(远程过程调用)。

这个例子的工作流程如下。

·当客户端启动时,它创建了匿名的单独的回调queue。

·客户端实现RPC请求时将同时设置两个属性:设置回调queue的replyTo,以及为每个请求设置唯一值correlationId。

·请求被发送到一个名为rpc_queue的queue中。

·RPC worker或者server一直在等待那个queue的请求。当请求到达时,通过在replyTo指定的queue来回复一个消息给客户端。

·客户端一直等待回调queue的数据。当消息到达时,检查correlationId属性,如果该属性和它请求的值一致,那么就返回响应结果给程序。

RPC示意如下图所示。

精读分布式系统核心:面向服务的分布式架构,消息通信常用模式图7-7 RPC示意图

下面是一个RPC的代码示例。

public class RPCServer {
private static final Logger LOGGER = LoggerFactory.getLogger(RPCServer.class);private static final String RPC_QUEUE_NAME = "rpc_queue";
private static int fib(int n) {
if (n == 0)
return 0;
if (n0==1)
return 1;
return fib(n-1) + fib(n-2);
}
public static void main(String[] argv) {
Connection connection = null;
Channel channel = null;
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
LOGGER.info(" [x] Awaiting RPC requests");
while (true) {
String response = null;
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
BasicProperties props = delivery.getProperties();
BasicProperties replyProps = new BasicProperties.Builder()
.correlationId(props.getCorrelationId())
.build();
try {
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
LOGGER.info(" [.] fib(" + message + ")");
response = "" + fib(n);
} catch (Exception e) {
LOGGER.error(" [.] " + e.toString());
response = "";
} finally {
channel.basicPublish("", props.getReplyTo(), replyProps,
response.getBytes("UTF-8"));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (Exception ignore) {
}
}
}
}
}

写在最后

如果你觉得自己学习效率低,缺乏正确的指导,可以加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧!
[Java架构群]
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的JAVA交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。
在这里插入图片描述

码字不易,如果觉得本篇文章对你有用的话,请给我一键三连!关注作者,后续会有更多的干货分享,请持续关注!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图灵课堂诸葛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值