ActiveMQ 是完全基于 JMS 规范实现的一个消息中间件产品。 是 Apache 开源基金会研发的消息中间件。
ActiveMQ 主要应用在分布式系统架构中,帮助构建高可用、高性能、可伸缩的企业级面向消息服务系统 。
ActiveMQ 默认采用61616端口提供JMS服务,使用8161端口提供管理控制服务
从 JMS 规范来了解 ActiveMQ
JMS定义
java 消息服务(java message service) 是java平台中关于面向消息中间件的API,
用于在两个应用程序之间或者分布式系统中发送消息,进行异步通信。
JMS 是一个与具体平台无关的API ,是 MOM(message oriented middleware) (面向消息中间件)的一个实现
MOM
MOM 是面向消息的中间件,使用消息传送提供者来协调消息传送操作。MOM需要提供API 和管理工具。客户端使用API 调用,把消息发送到由提供者管理的目的地。在发送消息之后,客户端会继续执行其他工作,并且在接受方收到这个消息确认之前,提供者一致保留该消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1TA4KVDs-1598836494488)(1547190603290.png)]
MOM 的特点
- 消息异步接受,发送者不需要等待消息接受者响应
- 消息可靠接受,确保消息在中间件可靠保存。只有接受放收到后才会删除消息
消息传递域
JMS 规范中定义了两种消息传递域:
- 点对点 (point-to-point) :类似 qq 单人聊天,私聊的消息只能被私聊的人能收到。
- 发布/订阅 (pub/sub) :类似qq群发消息,所有群成员都能收到消息
点对点消息传递域
- 每个消息只能有一个消费者
- 消息的生产者和消费者之间没有时间上的相关性。
- 无论消费者在消息发送的时候是否处于运行状态,都能收到消息
发布订阅消息传递域
- 每个消息可以有多个消费者
- 消息的生产者和消费者之间有时间上的相关性。
- 订阅一个主题的消费者只能消费自它订阅之后发布的消息
消息体
就是传送消息的时候的消息内容,JMS API 中定义了5中消息体格式,
消息体格式 | 对象 |
---|---|
TextMessage | String 对象 ,如 xml 文件内容 |
MapMessage | 名/值 对的集合,名是String 类型,值是Object |
ByteMessage | 字节流 |
StreamMessage | 输入输出流 |
ObjectMessage | 可序列化的对象 |
ActiveMQ 基本操作
P2P 和 PUB/SUB 消息
P2P 消息传递域
/**
* @author : guaoran
* @Description : <br/>
* 服务端 创建一个queue p2p 的消息
* 每个消息只能有一个消费者
* 消息的生产者和消费者之间没有时间上的相关性。
* 无论消费者在消息发送的时候是否处于运行状态,都能收到消息
* @date :2018/11/8 14:53
*/
public class JMSQueueProducer {
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory =
new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL);
Connection connection = null;
try {
//创建连接
connection = connectionFactory.createConnection();
connection.start();
//创建session会话
Session session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//创建目的地
Destination destination = session.createQueue("myQueue");
//创建消息发送者
MessageProducer producer = session.createProducer(destination);
//创建消息,并发送
TextMessage message = session.createTextMessage("Hello world!");
producer.send(message);
session.commit();
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if(connection != null){
connection.close();
}
}
}
}
/**
* @author : guaoran
* @Description : <br/>
* 消费端 接受一个p2p 的消息
* 每个消息只能有一个消费者
* 消息的生产者和消费者之间没有时间上的相关性。
* 无论消费者在消息发送的时候是否处于运行状态,都能收到消息
* @date :2018/11/8 14:53
*/
public class JMSQueueReceiverConsumer {
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory =
new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL);
Connection connection = null;
try {
//创建连接
connection = connectionFactory.createConnection();
connection.start();
//创建session会话
Session session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//创建目的地
Destination destination = session.createQueue("myQueue");
MessageConsumer consumer = session.createConsumer(destination);
//用来接收消息 ,可以采用阻塞式或监听式
//如果当前没有消息,则会进行阻塞
TextMessage message = (TextMessage) consumer.receive();
System.out.println(message.getText());
session.commit();
session.close();
/**
//用来接收消息,监听器的方式
MessageListener listener = new MessageListener() {
@Override
public void onMessage(Message message) {
try {
System.out.println(((TextMessage)message).getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
};
while(true){
//如果有消息则会循环进行消费,如果没有则循环进行监听
consumer.setMessageListener(listener);
session.commit();
}
*/
} catch (JMSException e) {
e.printStackTrace();
}finally {
if(connection != null){
connection.close();
}
}
}
}
PUB/SUB 消息传递域
/**
* @author : guaoran
* @Description : <br/>
* 服务端,sub/pub ,订阅消息:每个消息有多个消费者;类似群发消息,
* 发布的消息是根当前时间有相关性,
* 如果在发送消息前,消费者已经存在,则可以正常接收消息,
* 如果在发现消息后,消费者才存在,则在存在前的消息将被忽略
* @date :2018/11/8 14:53
*/
public class JMSTopicProducer {
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory =
new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL);
Connection connection = null;
try {
//创建连接
connection = connectionFactory.createConnection();
connection.start();
//创建session会话
Session session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//创建目的地
Topic topic = session.createTopic("myTopic");
//创建消息发送者
MessageProducer producer = session.createProducer(topic);
//创建消息,并发送
TextMessage message = session.createTextMessage("Hello world!");
producer.send(message);
session.commit();
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if(connection != null){
connection.close();
}
}
}
}
/**
* @author : guaoran
* @Description : <br/>
* 服务端,sub/pub ,订阅消息:类似群发消息,
* 发布的消息是根当前时间有相关性,
* 如果在发送消息前,消费者已经存在,则可以正常接收消息,
* 如果在发现消息后,消费者才存在,则在存在前的消息将被忽略
* 在上述的问题下,需要配置进行消费者的注册,
* connection.setClientID("001");
* session.createDurableSubscriber(topic,"001");
* @date :2018/11/8 14:53
*/
public class JMSPersistentTopicConsumer {
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory =
new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL);
Connection connection = null;
try {
//创建连接
connection = connectionFactory.createConnection();
connection.setClientID("001");
connection.start();
//创建session会话
Session session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//创建目的地
Topic topic = session.createTopic("myTopic");
//创建消息订阅者
MessageConsumer consumer = session.createDurableSubscriber(topic,"001");
//用来接收消息
//如果当前没有消息,则会进行阻塞
TextMessage message = (TextMessage) consumer.receive();
System.out.println(message.getText());
session.commit();
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if(connection != null){
connection.close();
}
}
}
}
消息的可靠性机制
一般来说,生产者创造消息将消息发送到消息中间件上,我们需要保证消息中间件上的消息,只有被消费者确认过之后才会被签收。
所以,消息的消费通常包含3个阶段:消费者接受消息,消费者处理消息,消息被确认。
因此,JMS 提供了事务性会话和非事务性会话
如果是事务性会话,接口提供了 commit 和 rollback 方法,与 jdbc 事务类似
事务性会话 和 非事务性会话
-
生产者生产消息
-
如果会话是事务会话,当执行commit()时才会将消息提交到mq上,否则,消费端不会消费到此消息
-
如果会话是非事务会话,不存在commit和rollback,都会将消息提交到mq上
-
public class Producer { public static void main(String[] args) throws JMSException { ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL); Connection connection = null; try { //创建连接 connection = connectionFactory.createConnection(); connection.start(); //创建session会话 Session session = connection.createSession(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE); /** * Boolean.TRUE :表示是事务性会话,只有当执行commit()时才会将消息提交到mq上,否则,消费端不会消费到此消息 * Boolean.FALSE: 表示是非事务性会话,不存在commit和rollback,都会将消息提交到mq上 */ //创建目的地 Destination destination = session.createQueue("myQueue"); //创建消息发送者 MessageProducer producer = session.createProducer(destination); //创建消息,并发送 TextMessage message = session.createTextMessage("Hello world!"); producer.send(message); //如果设置了 createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE); // 必须commit 否则将不会将消息提交到 Broker,消费者更不会受到 // session.commit(); session.close(); } catch (JMSException e) { e.printStackTrace(); }finally { if(connection != null){ connection.close(); } } } }
-
-
消费者消费消息
-
如果是事务性会话,只有当执行commit()时才会将告知mq已经消费消息,则删除该消息,否则,该消息会一直存在,
-
如果是非事务性会话,不存在commit和rollback,都会将消息删除
-
/** * @author : guaoran * @Description : <br/> * 会话存在的机制 * 事务性和非事务性 * acknowledgeMode * static final int AUTO_ACKNOWLEDGE = 1;//自动确认 * static final int CLIENT_ACKNOWLEDGE = 2;//客户端手动确认 * static final int DUPS_OK_ACKNOWLEDGE = 3;//延迟确认,可能会出现消息重复发送 * static final int SESSION_TRANSACTED = 0; * @date :2018/11/8 14:53 */ public class AcknowledgeModeConsumer { public static void main(String[] args) throws JMSException { ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL); Connection connection = null; try { //创建连接 connection = connectionFactory.createConnection(); connection.start(); //创建session会话 Session session = connection.createSession(Boolean.TRUE,Session.CLIENT_ACKNOWLEDGE); /** * Boolean.TRUE :表示是事务性会话,只有当执行commit()时才会将告知mq已经消费消息,则删除该消息,否则,该消息会一直存在, * Boolean.FALSE: 表示是非事务性会话,不存在commit和rollback,都会将消息删除 */ //创建目的地 Destination destination = session.createQueue("myQueue"); MessageConsumer consumer = session.createConsumer(destination); //用来接收消息 TextMessage message = (TextMessage) consumer.receive();//如果当前没有消息,则会进行阻塞 System.out.println(message.getText()); //当Session session = connection.createSession(Boolean.FALSE,Session.CLIENT_ACKNOWLEDGE); //需要客户端手动确认签收, /** * 在多条消息中,如果该消息之前的消息都没有进行手动签收,而该消息进行手动签收, * 则会导致,该消息及该消息之前的消息全部会被签收,该消息之后的消息不受影响 */ message.acknowledge(); session.close(); } catch (JMSException e) { e.printStackTrace(); }finally { if(connection != null){ connection.close(); } } } }
-
持久订阅和非持久订阅
持久订阅:可以持久化到硬盘中,MQ重启或宕机后消息不会丢失。
非持久订阅:保存到内存中,MQ重启或宕机后消息会丢失。
持久化消息和非持久化消息的发送策略
消息同步发送和异步发送
ActiveMQ 支持同步、异步两种发送模式将消息发送到 broker 上
同步发送
过程中,发送者发送一条消息会阻塞,直到broker 反馈一个确认消息,表示消息已经被broker处理。这个机制提供了消息的安全性保障,但是由于是阻塞的操作,会影响到客户端消息发送的性能。
异步发送
过程中,发送者不需要等待broker 提供反馈,所以性能相对较高。但是可能会出现消息丢失的情况。所以使用异步发送的前提是在某些情况下允许出现数据丢失的情况。
默认情况下,非持久化消息是异步发送的,持久化消息且在非事务模式下是同步发送的。
但是在***开启事务的情况下,消息都是异步发送的***。由于异步发送的效率会比同步发送性能更高。所以在发送持久化消息的时候,尽量去开启事务会话。
除了持久化消息和非持久化消息的同步和异步特性以外,我们可以通过以下几种方式来设置异步发送
//设置异步发送
ConnectionFactory connectionFactory =
new ActiveMQConnectionFactory(CommonUtil.CONNECT_URL+"?jms.useAsyncSend=true");
//也可以这样设置异步发送
((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true);
Connection connection = null;
//也可以这样设置异步发送
((ActiveMQConnection)connection).setUseAsyncSend(true);
消息的持久化策略分析
持久化存储支持的类型
vi activemq.xml
ActiveMQ 支持多种不同的持久化方式,主要有以下几种
- KahaDB 存储(默认方式)
- JDBC 存储
- LevelDB 存储
- Memory 存储
- JDBC With ActiveMQ Journal 存储
KahaDB 存储
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。
KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。在Kaha中,数据被追加到
data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。
KahaDB的配置方式
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
KahaDB的存储原理
在data/kahadb这个目录下,会生成四个文件
- db.data 它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-*.log里面存储的消息
- db.redo 用来进行消息恢复
- db-*.log 存储消息内容。新的数据以APPEND的方式追加到日志文件末尾。属于顺序写入,因此消息存储是比较快的。默认是32M,达到阀值会自动递增
- lock文件 锁,表示当前获得kahadb读写权限的broker
JDBC 存储
使用JDBC持久化方式,数据库会创建3个表:activemq_msgs,activemq_acks和activemq_lock。
ACTIVEMQ_MSGS 消息表,queue和topic都存在这个表中
ACTIVEMQ_ACKS 存储持久订阅的信息和最后一个持久订阅接收的消息ID
ACTIVEMQ_LOCKS 锁表,用来确保某一时刻,只能有一个ActiveMQ broker实例来访问数据库
JDBC的配置方式
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#Mysql-DS" createTablesOnStartup="true" />
</persistenceAdapter>
dataSource指定持久化数据库的bean,createTablesOnStartup是否在启动的时候创建数据表,默认值是true,这样每次启动都会去创建数据表了,一般是第一次启动的时候设置为true,之后改成false
Mysql持久化Bean配置
<!--在 <beans></beans> 里面-->
<bean id="Mysql-DS" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/activemq?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
添加 jar 包依赖 ./lib/
- commons-dbcp-1.4.jar
- commons-pool-1.6.jar
- mysql-connector-java-5.1.35.jar
LevelDB 存储
LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供
了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。
不过,据ActiveMQ官网对LevelDB的表述:LevelDB官方建议使用以及不再支持,推荐使用的是KahaDB
LevelDB的配置方式
<persistenceAdapter>
<levelDBdirectory="activemq-data"/>
</persistenceAdapter>
Memory 存储
基于内存的消息存储,内存消息存储主要是存储所有的持久化的消息在内存中。persistent=”false”,表示不设置持
久化存储,直接存储到内存中
Memory 的配置方式
<!--在 <beans></beans> 里面-->
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}" persistent="false">
......
</broker>
JDBC With ActiveMQ Journal 存储
这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库和读库。
ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。
当消费者的消费速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。
举个例子,生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况
下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上的消息,那么这个时候只需要同步剩余的
10%的消息到DB。
如果消费者的消费速度很慢,这个时候journal文件可以使消息以批量方式写到DB。
配置方式
<persistenceFactory>
<journalPersistenceAdapterFactory dataSource="#Mysql-DS" dataDirectory="activemqdata"/>
</persistenceFactory>