ActiveMQ消息队列学习(三)
ActiveMQ的介绍之前已经介绍过了,而且网上介绍性的东西比较多,在这里就不再去赘述了,本节将结合之前搭建的activeMQ集群完成消息队列的学习。整个思路是先搞定一个实用的demo,然后根据demo进行分析。
由于ActiveMQ是JMS规范的一个实现,所以先放置一个图来梳理下,然后针对图中的内容结合ActiveMQ进行总结归纳。
首先,先通过几个demo体验下activeMQ的使用。
ActiveMQ 点对点消息传递模型
首先来个图示意一下:
类似于上图,多个客户端通信也可以基于一个消息队列完成。每个客户端都会连接到中间的QUEUE上,在此可以看到你可能会有疑问,在这里插入图片描述不是说点对点吗?怎么画出2个客户端?这里指的点对点是指的是客户端和服务器两者的通信方式,意思是服务端和客户端直接通信,很像之前的P2P网络模型。本质上可以抽象为两个客户端和一个足则队列,P2P本质上没有太多的抽象概念。
那接下来先展示测试demo:
首先,消息的生产方:
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueuePRovider {
private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
public static final String BROKER_URL = "tcp://101.200.174.179:61616";
public static final String QUEUE_NAME = "first-mq-queue";
private ConnectionFactory connectionFactory = null;
private Connection connection = null;
private Session session = null;
// 目的地 --> 消息存储的位置
private Destination destination = null;
// 消息生产者
private MessageProducer messageProducer = null;
public static void main(String[] args) {
new QueuePRovider().doSendMessage();
}
public void doSendMessage() {
try{
// 创建连接,在于spring整合等,会使用到池化技术减少重复连接带来的开销
connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
connection = connectionFactory.createConnection();
// start
connection.start();
// create session
session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
// 发送消息到目的地
destination = session.createQueue(QUEUE_NAME);
messageProducer = session.createProducer(destination);
// 设置超时
messageProducer.setTimeToLive(60000);
for (int i = 0; i < 5; i++){
TextMessage message = session.createTextMessage("send content: kitty" + i);
messageProducer.send(message);
}
session.commit();
System.out.println("消息发送完成!");
messageProducer.close();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
那再看下消费者的实现:
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueReceiver {
public static final String BROKER_URL = "tcp://101.200.174.179:61616";
public static final String QUEUE_NAME = "first-mq-queue";
private ConnectionFactory connectionFactory;
private Connection connection;
private Session session;
private Destination destination;
private MessageConsumer consumer;
public static void main(String[] args) {
new QueueReceiver().doReceviceMessage();
}
public void doReceviceMessage(){
try {
connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
connection = connectionFactory.createConnection();
connection.start();
session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
destination = session.createQueue(QUEUE_NAME);
consumer = session.createConsumer(destination);
// 通过消息类型监听的方式
receiveByListener();
// reciveByManualWay();
}catch (Exception e){
e.printStackTrace();
}
}
private void receiveByListener(){
try{
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage){
try {
System.out.println("Received:" + ((TextMessage) message).getText());
// 确认消息签收
message.acknowledge();
session.commit();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
}catch (Exception e){
}
}
private void reciveByManualWay(){
while (true){
try {
TextMessage message = (TextMessage) consumer.receive(60000);
if (message != null){
System.out.println("Received:" + message.getText());
session.commit();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
此时可以登陆actvemq的admin去产看队列的消息队列的使用情况:
可以看到我们的使用情况,当时用listener方式读取信息时,recevier线程会被阻塞,一旦队列有消息(含过滤),就会触发监听的listener,使得这当前的recevier继续得到消息。
特点
- 每个消息只有一个消费者(Consumer),消息一旦被消费就会从队列中移除。
- 发送者和接收者在时间上没有依赖性(异步),即发送者发送消息时接收者无需处于运行状态,同样接收者接收消息时发送者无需处于运行状态。
- 接收者接收消息之后需向队列应答成功
ActiveMQ的发布/订阅模型
首先,这个pub/sub模型类似于一种设计模式,就是观察者模式,本质上是客户端注册监听某一个消息队列,当这个消息队列有变化的时候会通知注册当前这个队列的所有的订阅者,再activeMQ中一般默认为主动推送模式,也即队列有消息,会将消息主动推送给注册的客户端。本质上也和event/listener模型也有些相似。看下下面的示意图:
在示意图中可以看到,若client2和3注册了topic消息队列,此时使用client1来作为生产者生产消息,并发送到消息队列中,此时,消息队列会分发给所有的注册者,在这个图中也就是client2和client3。默认的pub/sub模型中,订阅者仅能够订阅到其subscribe之后的消息,也就是说,在其订阅之前的消息就接受不到了。而如果想接收的话当然也有其他办法,且慢慢递进吧。
发布/订阅模型的Java代码实现和P2P的基本一致,所以在下面的代码中仅仅展示改动的部分:
provider:
connection = connectionFactory.createConnection();
// start
connection.start();
// create session
session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
// 发送消息到目的地
// 注意:发布订阅模型需要使用createTopic
destination = session.createTopic(QUEUE_NAME);
messageProducer = session.createProducer(destination);
可以看到,当我们启动provider后消息队列中有了消息,在这里注意,我们这里使用的是默认的subscribe,也就是说我们这次发上去的消息,消费者目前是消费不了的,如果想要把这之前的消息也需要消费,则可以设置客户端id以及使用createDurableSubscriber方法完成。
consumer:
connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
connection = connectionFactory.createConnection();
connection.start();
session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//同样的,订阅主题才能够小队对应主题topic中的消息
destination = session.createTopic(QUEUE_NAME);
consumer = session.createConsumer(destination);
// 通过消息类型监听的方式
receiveByListener();
特点
- 每个消息可以有多个消费者
- 发布者和订阅者之间有时间上的依赖性。订阅者必须保持持续的运行状态才能接收到消息
- JMS提供了持久化订阅,这时候,订阅者未连接主题时(发布者)发布的消息,将在订阅者重新连接时进行重新发布,以便订阅者接收消息
ActiveMQ中的消息
消息队列是以消息为操作对象的,上面的例子仅仅使用了TextMassage类型的消息,而active MQ中还有其他的数据类型,所以也需要去学习下其他的消息类型以及消息使用的场景。
ActiveMQ 消息定义
首先,ActiveMQ中的消息是有三部分构成:
消息头
每条JMS 消息都必须具有消息头。头字段包含用于路由和识别消息的值。可以通过多种方式来设置消息头的值:
- 由JMS 提供者在生成或传送消息的过程中自动设置
- 由生产者客户机通过在创建消息生产者时指定的设置进行设置
- 由生产者客户机逐一对各条消息进行设置
以TextMassage为例,其继承了抽象类Message类,其实本质上有点类似于设置http协议的消息一样,ActiveMQ的消息头也会保留目的地址,消息类型,消息id,超时信息等:
public abstract class Message extends BaseCommand implements MarshallAware, MessageReference {
public static final String ORIGINAL_EXPIRATION = "originalExpiration";
/**
* The default minimum amount of memory a message is assumed to use
*/
public static final int DEFAULT_MINIMUM_MESSAGE_SIZE = 1024;
// 消息id
protected MessageId messageId;
// 对应消息队列的位置
protected ActiveMQDestination originalDestination;
// 事务ID
protected TransactionId originalTransactionId;
// 生产者id
protected ProducerId producerId;
protected ActiveMQDestination destination;
protected TransactionId transactionId;
protected long expiration;
protected long timestamp;
protected long arrival;
protected long brokerInTime;
protected long brokerOutTime;
protected String correlationId;
protected ActiveMQDestination replyTo;
protected boolean persistent;
protected String type;
protected byte priority;
protected String groupID;
protected int groupSequence;
protected ConsumerId targetConsumerId;
protected boolean compressed;
protected String userID;
protected ByteSequence content;
protected ByteSequence marshalledProperties;
protected DataStructure dataStructure;
protected int redeliveryCounter;
protected int size;
protected Map<String, Object> properties;
protected boolean readOnlyProperties;
protected boolean readOnlyBody;
protected transient boolean recievedByDFBridge;
protected boolean droppable;
protected boolean jmsXGroupFirstForConsumer;
private transient short referenceCount;
private transient ActiveMQConnection connection;
transient MessageDestination regionDestination;
transient MemoryUsage memoryUsage;
private BrokerId[] brokerPath;
private BrokerId[] cluster;
}
而TextMessage接口则定义了两个方法:
public interface TextMessage extends Message {
void setText(String string) throws JMSException;
String getText() throws JMSException;
}
本质上是获取TextMessage类型的内容,也就是放入消息内的Text,同样的不同的消息的接口定义了不同的设置和获取不同消息内容的方法,如ObjectMessage接口的定义:
public interface ObjectMessage extends Message {
void setObject(Serializable object) throws JMSException;
Serializable getObject() throws JMSException;
}
消息体(消息类型)
TextMessage文本消息(常用)
携带一个java.lang.String作为有效数据(负载)的消息,可用于字符串类型的信息交换。
ObjectMessage对象消息
携带一个可以序列化的Java对象作为有效负载的消息,可用于Java对象类型的信息交换。需要注意的是:在接收方要添加受信任的包。
创建User类的一个实例对象:
//对象消息类型
User user = new User();
user.setId(100);
user.setName("张三");
user.setAge(18);
Message message = session.createObjectMessage(user);
如果按照原来的方式在消费端处理消息则会报错:
Please take a look at htpp://...... for more information......
其实这说明说消息已经处理,但是没有接收到,可以到指定的网站上查看如何配置,所以查看网站提供的解决方式,在消息消费者端将对象所在的包添加为受信任的:
//1 .创建一个连接工厂
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
//添加受信任的包
List trustList = new ArrayList();
trustList.add("com.bjpowernode.activemq.model");
connectionFactory.setTrustedPackages(trustList);
MapMessage映射消息
携带一组键值对的数据作为有效负载的消息,其中Key必须为字符串,有效数据值必须是Java原始数据类型(或者它们的包装类)及String。
//映射消息
MapMessage message = session.createMapMessage();
message.setInt("age",10);
message.setString("name","hello kitty");
BytesMessage字节消息
携带一组原始数据类型的字节流(字节数组)作为有效负载的消息。需要注意的是:发送和接收的顺序必须一致。
//字节消息
BytesMessage message = session.createBytesMessage();
message.writeBoolean(true);
message.writeUTF("hello kitty");//写字符串方法
StreamMessage流消息
携带一个原始数据类型流作为有效负载的消息,它保持了写入流时的数据类型,写入什么类型,则读取也需要是相同的类型。和ByteMessage类型一样,也需要注意发送和接收的顺序必须一致。
StreamMessage message = session.createStreamMessage();
message.writeLong(1000L);
message.writeString("哈哈");
消息属性
消息属性是在我们设定了消息头,放入了消息内容后,还需要对消息的特性进行一些描述或者是设定,这时候则需要使用属性设置,类似于类的概念,属性是这个消息独有的一些有特点的。其中对于属性,在ActiveMQ中进行了定义:
public class ActiveMQMessage extends Message implements org.apache.activemq.Message, ScheduledMessage {
public static final byte DATA_STRUCTURE_TYPE = CommandTypes.ACTIVEMQ_MESSAGE;
public static final String DLQ_DELIVERY_FAILURE_CAUSE_PROPERTY = "dlqDeliveryFailureCause";
public static final String BROKER_PATH_PROPERTY = "JMSActiveMQBrokerPath";
// 定义了消息中的property,可以保存在JMS_PROPERTY_SETERS中随着消息一块传递给消费者
private static final Map<String, PropertySetter> JMS_PROPERTY_SETERS = new HashMap<String, PropertySetter>();
protected transient Callback acknowledgeCallback;
}
JMS的消息可靠性机制
JMS消息在被确认之后,会认为此消息是已经被成功消费了,一般的消息的消费包含三个阶段:客户端接收到消息,客户端处理消息,消息确认。
说到这里不得不提上面demo中的这句代码:
session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
- 这里的第一个参数true代表的是事务会话的开启与否,如果设置为true,则代表会话是事务的,需要使用session进行commit才能奏效,类似于数据库中的事务的过程。也就是说,当使用commit()方法提交后,消息才会被自动签收。那如果设置为False呢?如果设置为false,则表示需要根据创建会话时的应答模式决定,也就是第二个参数。
- 如果设置为Session.AUTO_ACKNOWLEDGE,则表示当从客户端成功receive方法返回以后,或者使用MessageListener调用onMessage方法成功返回后,会话会自动的确认签收该消息。
- 如果设置为CLIENT_ACKNOWLEDGE时,客户端通过调用消息的acknowledge()方法确认该消息。这里需要注意的是,如果我们发送了10个消息,如果我们在第5个调用了该方法,那么在这之前的所有消息都会被确认,后面的则不会。如果设置为DUPS_OK_ACKNOWLEDGE,则表示此消息可以被延时确认。
延伸一下
关于ActiveMQ的本地事务
在一个JMS客户端可以使用本地事务来组合消息的发送和接收。JMS Session接口提供了commit和rollback方法。PRovider会缓存每个生产者当前生产的所有消息,一直保存到commit或者是rollback;commit意味着将会导致事务中的所有消息会被持久化存储,同时也意味着生产的所有消息都会被发送。rollback则意味着provider会清除此事务下的所有的消息记录。在事务未提交之前消息是不会被持久化存储的,消息是会被销毁的,消费的消息则会被恢复,也就是说,消费端下次仍然能够接收到发送端的消息,除非这个消息设置了过期时间,消息过期了。同样的,被回滚的消息也不会被消费者消费。