消息队列

1.系统间通信

大型应用通常会拆分为多个子系统,那么,子系统之间如何进行通信呢?

1.1RPC

RPC是一种通过网络从远程计算机程序上请求服务。

  1. 一种协议,实现有:Dubbo、Thrift、GRPC等。
  2. 网络实现透明。
  3. 跨语言。

1.2消息队列

请求子系统发出请求消息,然后返回,消息队列进行传递消息。
目标子系统接收请求消息,然后根据消息做出相应的处理。

消息发布者只是保证发布正确的消息到消息队列。
消息使用者只从消息队列中取消息。

消息发布者不知道消息使用者的信息。
消息使用者不知道消息发布者的信息。

2.消息队列的优点

2.1解耦

每个子系统只关注自己的核心逻辑以及子系统与消息队列的通信。
这样随着子系统的数量或者复杂度的增加,子系统间通信的成本是相同,不管你增加多少个子系统,其中每一个子系统都只关注自身的逻辑以及与消息队列的交互。

举个简单的例子:
过去没有朋友圈的时候,你买了一件新衣服,那么你会穿上新衣服,然后走到亲戚朋友面前,让亲戚朋友评论。你有多少个亲戚朋友,就需要去让亲戚朋友看多少次。其中有些亲戚朋友很关心你的衣服漂不漂亮,等等。但是也有些亲戚朋友并不感兴趣。

现在有了微信,qq等等软件,你的亲戚朋友都加了你作为好友。那么你买了一件新衣服,需要拍张照片放到朋友圈或者空间里。然后关注你的亲戚朋友就会看到你发的照片,然后进行评论。

亲戚朋友点赞或者评论后,会及时的返回给你。

2.2流量削峰

这个名字就很通俗易懂。
出现短时间的高并发的时候,现将请求持久化,然后逐步的处理。从而削平高峰期的并发流量,改善系统的性能。

2.3日志收集

通过日志可以跟踪调试信息,定位问题等等。
但是日志用不好也有缺陷,比如无用日志记录,日志记录异常、日志阻塞,等等。
使用消息队列可以将日志收集作为一个单独的子系统进行处理。

2.4事务最终一致

如何对不同的数据表保持事务一致呢?
分布式事务。
XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。XA协议包括两套函数,以xa_开头的及以ax_开头的。

取决于上下文, XA 有多种意思. 我们常见的数据库连接事务中的 XA 是指由 X/Open 组织提出的分布式事务处理的规范. XA 规范主要定义了事务管理器(Transaction Manager)和局部资源管理器(Local Resource Manager)之间的接口.

以下的函数使事务管理器可以对资源管理器进行的操作:
1)xa_open,xa_close:建立和关闭与资源管理器的连接。
2)xa_start,xa_end:开始和结束一个本地事务。
3)xa_prepare,xa_commit,xa_rollback:预提交、提交和回滚一个本地事务。
4)xa_recover:回滚一个已进行预提交的事务。
5)ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
6)ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册。

【以上数据来源于百度百科】
X/Open XA
维基百科,自由的百科全书
跳到导航跳到搜索
在计算技术上,XA规范是开放群组关于分布式事务处理 (DTP)的规范。规范描述了全局的事务管理器与局部的资源管理器之间的接口。XA规范的目的是允许多个资源(如数据库,应用服务器,消息队列,等等)在同一事务中访问,这样可以使ACID属性跨越应用程序而保持有效。XA使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。

XA规范描述了资源管理器要支持事务性访问所必需做的事情。遵守该规范的资源管理器被称为XA compliant。
【以上数据来源于维基百科】

上述说的就是使用XA即分布式事务。但是分布式事务存在一定的问题,因为涉及到多个系统之间的通信,调用等等,导致其性能特别差,所以不适合在生产环境下有高并发和高性能要求的场景。

消息队列就比较容易解决这些问题。保持事务一致性很简单,保证发布两个消息即可。

3.消息队列的功能特点

3.1消息队列的一些术语

一个典型意义上的消息队列,至少需要包含消息的发送、接收和暂存。

Broker:消息处理中心,负责消息的接收、存储和转发等等。
Producer:消息生产者,负责产生和发送消息到处理中心。
Consumer:消息消费者,负责从消息处理中心获取信息,并进行相应的处理。

3.2消息堆积

消息队列的本质还是生产值消费者的处理模型。
因为生产者与消费者互相不知,所以就可能出现生产者速度大于消费者的速度。
当然消费者的消费速度大于生产速度也会出现。
但是相对于生产者速度大于消费者的后果相比,消费者消费速度大于生产者生产速度的后果要小的多。
消费者的消费速度大于生产者的生产速度,一般后果就是造成消息队列为空,这是允许的。
但是如果生产者的生产速度大于消费者的消费速度,后果就比较严重了:消息队列可能溢出,可能阻塞等等。

如何避免?

可以考虑给消息队列一个size,消息个数不允许大于size,否则拒绝接受新的消息。

3.3消息持久化

对于一个消息队列,如果消息到达消息处理中心后不做任何处理,直接转给消费者,那么消息队列就失去了意义。
所以需要消息队列把消息暂存,然后进行处理。

3.4可靠投递

可靠投递是不允许存在消息丢失的。从整个消息的生命周期来说,消息丢失可能发生在线面三个地方:

  1. 从生产者到消息处理中心;
  2. 从消息处理中心到消息消费者;
  3. 消息处理中心持久化。

说白了就三个:消息发布者到消息队列、消息队列到消息消费者、消息队列自己。

3.5消息重复

消息队列在不同的场景对于消息有着不同的需求:
论坛等要求消息快速发布

电商等要求消息可靠投递

所以不同的场景选择不同的处理逻辑。

3.6严格有序

对于很多的业务操作来说,其每个单独的步骤并不独立,比如:你在淘宝买东西:
显示挑选->下订单->付款->发货->快递->收货->完成订单。
这里面有很多是不能单独进行的,必须根据上下文环境进行处理。

3.7集群

现在的系统要求高可用,灾备等等。使用集群是不二选择。

3.8消息中间件

中间件:
非底层操作系统软件、非业务应用软件,不是直接给最终用户使用的,不能直接给客户带来价值的软件系统。

3.9MQ

IBM消息中间件MQ比其他的MQ来说更加的安全、简便、快速。其稳定性、可扩展性和跨平台性都很优越,还有强大的事物处理能力和消息通讯能力。其市场占有率最高。

4.例子

4.1角色

  1. 消息处理中心
  2. 消息生产者
  3. 消息消费者

4.2消息处理中心

package com.study.broker;

import java.util.concurrent.ArrayBlockingQueue;

/**
* 消息处理中心:消息的接收、存储、转发等
* @author jiayq
*/
public class Broker {

private final static int MAX_SIZE = 3;

private static ArrayBlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(MAX_SIZE);

public static void produce(String msg) {
if (messageQueue.offer(msg)) {
System.out.printf("成功投递消息%s,当前暂存的消息数量是:%d\n", msg, messageQueue.size());
} else {
System.out.printf("消息处理中心暂存达到最大值,无法暂存%s消息\n", msg);
}
System.out.println("-----------------------------------");
}

public static String consume(){
String msg = messageQueue.poll();
if (msg != null) {
System.out.printf("已经消费消息%s,暂存的消息数量是%d\n", msg, messageQueue.size());
} else {
System.out.println("没有暂存的消息了");
}
System.out.println("---------------------------------------");
return msg;
}

}

其中ArrayBlockingQueue是带有边界的队列。
​​​ArrayBlockingQueue传送门​

4.3Server

package com.study.server;

import com.study.broker.Broker;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
* 对外提供消息中心服务方法的类
* @author jiayq
*/
public class BrokerServer implements Runnable{

public static final int SERVER_PORT = 10001;

private final Socket socket;

public BrokerServer(Socket socket) {
this.socket = socket;
}

@Override
public void run() {

try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
while (true) {
String str = in.readLine();
if (str == null) {
continue;
}
System.out.println("接收到数据:" + str);
if ("CONSUME".equals(str)) {
String msg = Broker.consume();
out.println(msg);
out.flush();
} else {
Broker.produce(str);
}
}
} catch (Exception e) {
e.printStackTrace();
}

}

public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
while (true) {
BrokerServer brokerServer = new BrokerServer(serverSocket.accept());
new Thread(brokerServer).start();
}
}
}

4.4Client

package com.study.client;

import com.study.server.BrokerServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;

/**
* 客户端调用服务端
* @author jiayq
*/
public class MqClient {

public static void produce(String msg) throws IOException {
Socket socket = new Socket(InetAddress.getLocalHost(), BrokerServer.SERVER_PORT);
try (
PrintWriter out = new PrintWriter(socket.getOutputStream())

) {
out.println(msg);
out.flush();
}
}

public static String consume() throws IOException {
Socket socket = new Socket(InetAddress.getLocalHost(), BrokerServer.SERVER_PORT);
try (
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
out.println("CONSUME");
out.flush();
String msg = in.readLine();
return msg;

}
}

}
class ProduceClient{

public static void main(String[] args) throws IOException {
MqClient.produce("hello World");
}

}

class ConsumeClient{

public static void main(String[] args) throws IOException {
System.out.println(MqClient.consume());
}

}

4.5效果

先启动服务端:

消息中间件--MQ--消息队列_消息队列MQ


在启动客户端:

消息中间件--MQ--消息队列_消息队列MQ_02


消息中间件--MQ--消息队列_socket编程_03


多次生产:

消息中间件--MQ--消息队列_socket编程_04


消费:

消息中间件--MQ--消息队列_生产者消费者和消息队列_05


消息中间件--MQ--消息队列_socket编程_06


多次消费:

消息中间件--MQ--消息队列_生产者消费者和消息队列_07

4.6注意点

消息中间件--MQ--消息队列_消息队列MQ_08


一定是println或者print("…\n")

反正必须换行,否则会发生阻塞。

消息中间件--MQ--消息队列_MQ_09


消息中间件--MQ--消息队列_socket编程_10


在测试的时候出现一个问题:

可以生产,但是却无法消费,就是因为在客户端或者服务端写入消息时,没有换行。

那么即使flush了,在接收方也无法读取,因为在接收方调用的是readLine,因为没有换行,所以in.readLine认为行写入没有结束,导致读到的是空。

为什么生产者可以?
因为生产者没有in.readLine,在写入消息后整个生产者的客户端进程结束,面临被销毁,强制加入了换行吧。

代码仓库地址:
​​​https://github.com/a18792721831/MQ.git​