消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。如要详细了解可参考消息队列总结
一、MQ的两种模式
JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。而MQ则是消息队列服务,是面向消息中间件(MOM)的最终实现,是真正的服务提供者;MQ的实现可以基于JMS,也可以基于其他规范或标准。目前选择的最多的是ActiveMQ。
MQ在JMS规划下的两种经典模式。
第一种:点对点(point-to-point,简称PTP)模式
在点对点模式下,一个生产者向一个特定的队列发布消息,一个消费者从该队列中读取消息。这里,生产者知道消费者的队列,并直接将消息发送到消费者的队列。这种模式概括为:只有一个消费者将获得消息。生产者不需要在接收者消费该消息期间处于运行状态,接收者也同样不需要在消息发送时处于运行状态。每一个成功处理的消息都由接收者签收。
第二种:发布/订阅(publish/subscribe,简称pub/sub)模式
发布者/订阅者模式支持向一个特定的消息主题发布消息。0或多个订阅者可能对接收来自特定消息主题的消息感兴趣。在这种模式下,发布者和订阅者彼此不知道对方。这种模式好比是匿名公告板。这种模式概括:多个消费者可以获得消息。在发布者和订阅者之前存在时间依懒性。发布者需要建立一个订阅(subscription),以便客户能够订购。订阅者必须保持持续的活动状态以接收消息,除非订阅者建立了持久的订阅。在那种情况下,在订阅者未连接时发布的消息将在订阅者重新连接时重新发布。
FourInOne(中文名字“四不像”)是一个四合一分布式计算框架 。将Hadoop,Zookeeper,MQ,分布式缓存四大主要的分布式计算功能合为一个框架内,对复杂的分布式计算应用进行了大量简化和归纳。本文将通过写一个类ForuInOne的框架来实现这两种模式。
二、发送接收的队列模式(PTP)的实现
我们可以将Domain视为MQ队列,每个node为一个队列消息,检查Domain的变化来获取队列消息。
Sender:是一个队列发送者,它发送消息的实现是在queue上创建一个匿名节点来存放消息
p1.create(queue,(Serializable)obj);
Receiver:是一个队列接收者,他轮循queue上有没有最新消息,有就取出,并删除该节点,注意它是每次获取第一个消息,这样保证消息读取的顺序。如图2-1所示。
图 2-1 PTP模式实现
1)启动ParkServerDemo(它的IP端口如果与当前已有端口冲突可进行修改),IDEA中运行结果如图2-2所示:
图2-2 ParkServerDemo
2)运行Sender,结果如图2-3所示
图2-3 Sender
3)Receiver,结果如2-4所示
图2-4 Receiver
//ParkServerDemo
package rmi.rmi;
import rmi.fourinone.LogUtil;
import rmi.fourinone.ParkService;
import rmi.fourinone.ParkServiceImpl;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class ParkServerDemo {
public static void main(String[] args) {
try {
ParkService parkService = new ParkServiceImpl();
LocateRegistry.createRegistry(9985);
try {
Naming.bind("rmi://localhost:9985/ParkService", parkService);
LogUtil.info("[ParkServerDemo]", "[main]", "Start server!");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
//Sender
package rmi.rmi;
import rmi.fourinone.ParkService;
import java.io.*;
import java.rmi.RemoteException;
import static rmi.rmi.Utils.getNumber;
import static rmi.rmi.Utils.getPark;
import static rmi.rmi.Utils.toBytes;
public class Sender {
public static ParkService parkService = getPark();
public static void send(String queue, Object object) {
try{
parkService.create(queue, getNumber(), toBytes((Serializable) object));
}catch (RemoteException e){
e.printStackTrace();
}
return;
}
public static void main(String[] args) {
send("queue1", "hello");
send("queue1", "world");
send("queue2", "mq");
}
}
//Recevier
package rmi.rmi;
import rmi.fourinone.ObjValue;
import rmi.fourinone.ParkService;
import java.rmi.RemoteException;
import java.util.ArrayList;
import static rmi.rmi.Utils.getPark;
import static rmi.rmi.Utils.toObject;
public class Receiver {
public static ParkService parkService = getPark();
public static Object receive(String queue) throws RemoteException {
Object obj=null;
ObjValue objValue = parkService.get(queue,null);
ObjValue nodes = objValue.getWidely(queue + "..*");
ArrayList<String> nodeNames = nodes.getObjNames();
for(String nodeName:nodeNames) {
if (objValue != null && !nodeName.contains("version")) {
obj = getMsg(objValue, nodeName);
parkService.delete(queue, nodeName.split("\\.")[1]);
break;
}
}
return obj;
}
public static Object getMsg(ObjValue ov, String Domainnodekey) {
return toObject((byte[]) ov.getObj(Domainnodekey));
}
public static void main(String[] args) {
try{
System.out.println(receive("queue1"));
System.out.println(receive("queue1"));
System.out.println(receive("queue2"));
}catch (RemoteException e){
e.printStackTrace();
}
}
}
三、主题订阅模式的实现
我们可以将Domain视为订阅主题,将每个订阅者(Subscriber)注册到Domain的节点Node上,发布者(Publisher)将消息逐一更新每个节点,订阅者监控每个属于自己的节点的变化事件获取订阅消息,收到后清空内容等待下一个消息,多个消息用一个arraylist存放。
图3-1 发送接收模式实现
Publiser:是一个主题发布者,它通过p1.get(topic)获取topic(主题)的所有订阅者节点,并将消息更新到每个节点上,如果有多个追加到arraylist存放。
Subscriber:是一个消息订阅者,它通过subscrib(String topic, String subscribeName, LastestListener lister)
实现消息订阅,其中3个参数分别是主题名、订阅者名称、事件处理实现,这个接口会传入更新的节点内容对象,然后Subscriber用一个空的arraylist监控内容,等待下一次接收消息。HappenLastest有个boolean返回值,如果返回false,它会一直监控变化,继续有新的变化时还会进行事件调用;如果返回true,它完成本次事件调用后就终止。
1)启动ParkServerDemo(它的IP端口如果与当前已有端口冲突可进行修改),IDEA中运行结果如图3-2所示:
图3-2 ParkServerDemo
图3-3 Subscriber_a
图3-4 Publisher
运行Publisher开始投递消息,投递完成后Publisher退出,aaa订阅者的窗口显示如图3-5所示。
图3-5 Subscriber_a
完整demo源码如下:
//ParkServerDemo
package rmi.rmi;
import rmi.fourinone.LogUtil;
import rmi.fourinone.ParkService;
import rmi.fourinone.ParkServiceImpl;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class ParkServerDemo {
public static void main(String[] args) {
try {
ParkService parkService = new ParkServiceImpl();
LocateRegistry.createRegistry(9985);
try {
Naming.bind("rmi://localhost:9985/ParkService", parkService);
LogUtil.info("[ParkServerDemo]", "[main]", "Start server!");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
//Subscriber
package rmi.rmi;
import rmi.fourinone.*;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Iterator;
import static rmi.rmi.Receiver.getMsg;
import static rmi.rmi.Utils.getPark;
import static rmi.rmi.Utils.toBytes;
import static rmi.fourinone.ParkObjValue.getDomainnodekey;
public class Subscriber implements LastestListener {
public static ParkService parkService = getPark();
@Override
public boolean happenLastest(LastestEvent le) {
ObjValue ov = (ObjValue)le.getSource();
try{
// System.out.println(ov.keySet().size());
for(Iterator iter = ov.keySet().iterator(); iter.hasNext();){
// System.out.println(ov.keySet().size());
String curkey = (String)iter.next();
Object objArr = getMsg(ov, curkey);
ArrayList arr = (ArrayList) objArr;
System.out.println("published message:"+arr);
ObjValue newob = parkService.update(curkey.split("\\.")[0], curkey.split("\\.")[1],
toBytes((Serializable) new ArrayList()));
le.setSource(newob);
}
}catch (RemoteException e){
e.printStackTrace();
}
return false;
}
public static void subscrib(String topic, String subscribeName, LastestListener lister)
{
ArrayList arr = new ArrayList();
try {
ObjValue objValue = parkService.create(topic, subscribeName, toBytes((Serializable) arr));
addLastestListener(topic, subscribeName, objValue, lister);
}catch (RemoteException e){
e.printStackTrace();
}
}
public static void addLastestListener(String domain, String node, ObjValue ov, LastestListener liser) {
final String dm = domain;
final String nd = node;
final ObjValue oov = ov;
final LastestListener lis = liser;
new AsyncExector(){
public void task(){
try{
ObjValue oldov = oov;
while(true){
String sVersion = oldov.get(getDomainnodekey(domain, node) + "__version").toString();
long vid = oldov!=null?Long.parseLong(sVersion):0l;
ObjValue newov = parkService.getLastest(dm, nd, vid);
if(newov != null){
LogUtil.fine("[Park]","[Trim LastestEvent]","[obj]");
LastestEvent le = new LastestEvent(newov);
if(lis.happenLastest(le))
break;
oldov = (ObjValue) le.getSource();
}
}
}catch(Exception e){
LogUtil.info("[Park]","[addLastestListener]",e);
}
}
}.run();
}
public static void main(String[] args)
{
subscrib("topic1", args[0], new Subscriber());
subscrib("topic2", args[0], new Subscriber());
}
}
//Publisher
package rmi.rmi;
import rmi.fourinone.ObjValue;
import rmi.fourinone.ParkService;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.ArrayList;
import static rmi.rmi.Receiver.getMsg;
import static rmi.rmi.Utils.getPark;
import static rmi.rmi.Utils.toBytes;
public class Publisher {
public static ParkService parkService = getPark();
public static void publish(String topic, Object obj)
{
try {
ObjValue objValue = parkService.get(topic, null);
ObjValue nodes = objValue.getWidely(topic + "..*");
ArrayList<String> nodeNames = nodes.getObjNames();
if (objValue != null) {
for (String nodeName : nodeNames) {
if (nodeName.contains("version"))
continue;
Object objArr = getMsg(objValue, nodeName);
ArrayList arr = (ArrayList) objArr;
arr.add(obj);
parkService.update(nodeName.split("\\.")[0], nodeName.split("\\.")[1], toBytes((Serializable) arr));
}
}
}catch (RemoteException e){
e.printStackTrace();
}catch (NullPointerException e){
e.printStackTrace();
}
}
public static void main(String[] args)
{
//发布消息
publish("topic1", "Hello World");
publish("topic1", "Coder");
publish("topic2", "Day Day Up");
}
}
四、MQ的使用场景自我总结
MQ有以下几点特性:
- 解耦
在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。 - 冗余
有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的”插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 - 扩展性
因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。 - 灵活性
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。 - 可恢复性
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 - 顺序保证
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。 - 缓冲
在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。 - 异步通信
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
根据以上MQ的优势,可以总结出MQ主要在以下场景使用- 进程间通讯和系统间的消息通知,比如在分布式系统中。
- 解耦,比如像公司有许多开发团队,每个团队负责业务的不同模块,各个开发团队可以使用MQ来通信。
- 在一些高并发场景下,使用MQ的异步特性。
参考资料
[1]独立网页(狗蛋儿_312)“JAVA消息(第一篇)”(2018.5.30 ), [Online]Available:https://blog.csdn.net/weixin_37352094/article/details/80500202
[2]大规模分布式系统架构与设计实战.彭渊.北京:机械工业出版社,2014.1
[3]独立网页(HD243608836)“消息队列mq总结(2018.5.6 ), [Online]Available: https://blog.csdn.net/HD243608836/article/details/80217591
[4]独立网页(过眼浮云~~)Java常用消息队列原理介绍及性能对比”(2017.11.27)
[Online]Available: https://blog.csdn.net/songfeihu0810232/article/details/78648706
[5]独立网页(高尔夫golf)“关于消息队列的使用”(2016.8.15)[Online]Available: https://blog.csdn.net/konglongaa/article/details/52208273
[6]独立网页(shero_zsmj)阿里四不像——分布式核心技术框架 Fourinone”(2016.8.22)[Online]Available:https://blog.csdn.net/songfeihu0810232/article/details/78648706