消息中间件之ActiveMQ

什么是消息中间件?

消息中间件是利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据 通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,可以在分布式架构下扩展进程之间的通信。

消息中间件能做什么?

消息中间件主要解决的就是分布式系统之间消息传递的问题,它能够屏蔽各种平台以及协议之间的特性,实现引用程序之间的协同。举个非常简单的例子,就拿一个电商平台的注册功能来简单分析下,用户注册这一个服务,不单单只是insert一条数据到数据库里面就完事了,还需要发送激活邮件、发送新人红包或者积分、发送营销短信等一系列操作。假如说这里面的每一个操作,都需要消耗1s,那么整个注册过程就需要耗时4s才能响应给用户。 

 

 

 但是我们从注册这个服务可以看到,每一个子操作都是相对独立的,同时,基于领域划分以后,发送激活邮件、发送营销短信、赠送积分及红包都属于不同的子域。所以我们可以对这些子操作进行来实现异步化执行,类似于多线程并行处理的概念。如何实现异步化呢?用多线程能实现吗?多线程当然可以实现,只是,消息的持久化、消息的重发这些条件,多线程并不能满足。所以需要借助一些开源中间件来解决。而分布式消息队列就是一个非常好的解决办法,引入分布式消息队列以后,架构图就变成这样了(下图是异步消息队列的场景)。通过引入分布式队列,就能够大大提升程序的处理效率,并且还解决了各个模块之间的耦合问题 

 

 

 我们再来展开一种场景,通过分布式消息队列来实现流量整形,比如在电商平台的秒杀场景下,流量会非常大。通过消息队列的方式可以很好的缓解高流量的问题 

 

 

 

➢ 用户提交过来的请求,先写入到消息队列。消息队列是 有长度的,如果消息队列长度超过指定长度,直接抛弃
➢ 秒杀的具体核心处理业务,接收消息队列中消息进行处 理,这里的消息处理能力取决于消费端本身的吞吐量

消息中间件还有更多应用场景,比如在弱一致性事务模型中,可以采用分布式消息队列的实现最大能力通知方式来实现数据的最终一致性等待

ActiveMQ简介

ActiveMQ是完全基于JMS规范实现的一个消息中间件产品。是Apache开源基金会研发的消息中间件。ActiveMQ主要应用在分布式系统架构中,帮助构建高可用、高性能、可伸缩的企业级面向消息服务的系统

特性

1.多语言和协议编写客户端

语言:java/C/C++/C#/Ruby/Perl/PHP

应用协议:openwire/stomp/REST/ws/notification/XMPP/AMQP

2.完全支持jms1.1和J2ee1.4规范

3.对spring的支持,ActiveMQ可以很容易内嵌到Spring模块中

ActiveMQ安装

1.登录到 http://activemq.apache.org/activemq-5150release.html,找到ActiveMQ的下载地址
2.直接copy 到服务器上通过tar -zxvf apacheactiveMQ.tar.gz
3. 启动运行
a) 普通启动:到bin目录下, sh activemq start
b) 启动并指定日志文件 sh activemq start > /tmp/activemqlog
4. 检查是否已启动 ActiveMQ默认采用61616端口提供JMS服务,使用8161 端口提供管理控制台服务,执行以下命令可以检查是否 成功启动ActiveMQ服务 netstat -an|grep 61616
5. 通过 http://192.168.11.156:8161 访问 activeMQ 管理页 面 ,默认帐号密码 admin/admin
6. 关闭ActiveMQ; sh activemq stop

从 JMS 规范来了解 ActiveMQ 

JMS 定义


Java消息服务(Java Message Service)是java平台中关于面向消息中间件的API,用于在两个应用程序之间,或者分布式系统中发送消息,进行异步通信。
JMS 是一个与具体平台无关的 API,绝大多数 MOM (Message Oriented Middleware)(面向消息中间件)提供商都对JMS提供了支持。今天给大家讲的ActiveMQ就是其中一个实现

什么是MOM

MOM是面向消息的中间件,使用消息传送提供者来协调消息传送操作。MOM需要提供API和管理工具。客户端使用api调用,把消息发送到由提供者管理的目的地。在发送消息之后,客户端会继续执行其他工作,并且在接受放收到这个消息确认之前,提供者一直保留该消息

 

 

 MOM特点:

1.消息异步接受,发送者不需要等待消息接收者响应

2.消息可靠接受,确保消息在中间件可靠保存。只有接受方收到后才删除消息

Java 消息传送服务规范最初的开发目的是为了使 Java 应用程序能够访问现有MOM系统。引入该规范之后,它已被许多现有的 MOM 供应商采用并且已经凭借自身的 功能实现为异步消息传送系统

其他开源的JMS提供商

JbossMQ(jboss4)、jboss messaging(jboss5)、joram、 ubermq、mantamq、openjms… 大部分基于的JMS provider开源的消息中间件都已经停止 维护了,剩下的几个都抱到了大腿,比如Jboss mq和jboss、
joram 与 jonas(objectweb 组织) 、 ActiveMQ 与 Geronimo(apache基金组织)。

JMS 规范

我们已经知道了 JMS 规范的目的是为了使得 Java 应用程 序能够访问现有 MOM (消息中间件)系统,形成一套统一 的标准规范,解决不同消息中间件之间的协作问题。在创 建JMS规范时,设计者希望能够结合现有的消息传送的精 髓,比如说
1. 不同的消息传送模式或域,例如点对点消息传送和发布 /订阅消息传送
2. 提供于接收同步和异步消息的工具
3. 对可靠消息传送的支持
4. 常见消息格式,例如流、文本和字节

 

 

 细化JMS的基本功能

通过前面的内容讲解以及案例演示,我们已经知道了 JMS 规范以及他的基本功能是用于和面向消息中间件相互通信 的应用程序的接口,那么JMS提供的具体标准有哪些呢? 我们来仔细去研究下
消息传递域
JMS 规范中定义了两种消息传递域:点对点(point-topoint )消息传递域 和发布/ 订阅 消息传递域 (publish/subscribe) 简单理解就是:有点类似于我们通过qq聊天的时候,在群 里面发消息和给其中一个同学私聊消息。在群里发消息, 所有群成员都能收到消息。私聊消息只能被私聊的学员能 收到消息,

点对点消息传递域

1. 每个消息只能有一个消费者
2. 消息的生产者和消费者之间没有时间上的相关性。无论 消费者在生产者发送消息的时候是否处于运行状态,都 可以提取消息

 

 发布订阅消息传递域 

1. 每个消息可以有多个消费者 2. 生产者和消费者之间有时间上的相关性。订阅一个主题 的消费者只能消费自它订阅之后发布的消息。JMS 规范 允许客户创建持久订阅,这在一定程度上降低了时间上 的相关性要求。持久订阅允许消费者消费它在未处于激 活状态时发送的消息

 

 消息结构组成

JMS消息由几部分组成:消息头、属性、消息体

消息头(Header)-消息头包含消息的识别信息和路由信息,消息偷包含一些标准的属性如:

JMSDestination     消息发送目的地,queue或者topic

JMSDeliveryMode   传送模式。持久模式和非持久模式

JMSPriority       消息优先级(优先级分为10个级别,从0(最低)到9(最高),如果不设定优先级,默认级别是4.需要注意的是,JMS provider并不一定保证按照优先级的顺序提交消息)

JMSMessageID     唯一识别每个消息的标识

属性
按类型可以分为应用设置的属性,标准属性和消息中间件 定义的属性 1. 应用程序设置和添加的属性,比如 Message.setStringProperty(“key”,”value”); 通过下面的代码可以获得自定义属性的,在接收端的代 码中编写
在发送端,定义消息属性
message.setStringProperty("Mic","Hello
World");
在接收端接收数据

Enumeration enumeration=message.getPropertyNames(); 
while(enumeration.hasMoreElements()){ 
    String 
name=enumeration.nextElement().toString(); 
    
System.out.println("name:"+name+":"+messag
e.getStringProperty(name)); 
    System.out.println(); 
}

3. JMS provider特定的属性
消息体
就是我们需要传递的消息内容,JMS API 定义了 5 中消息体格式,可以使用不同形式发送接收数据,并可以兼容现 有的消息格式,其中包括
TextMessage java.lang.String 对象,如 xml 文件内 容
MapMessage 名/值对的集合,名是String对象,值 类型可以是Java任何基本类型
BytesMessage 字节流
StreamMessage Java中的输入输出流
ObjectMessage Java中的可序列化对象
Message 没有消息体,只有消息头和属性。
绝大部分的时候,我们只需要基于消息体进行构造

持久订阅
持久订阅的概念,也很容易理解,比如还是以 QQ 为例, 我们把 QQ退出了,但是下次登录的时候,仍然能收到离 线的消息。 持久订阅就是这样一个道理,持久订阅有两个特点:
1. 持久订阅者和非持久订阅者针对的Domain是Pub/Sub, 而不是P2P
2. 当 Broker 发送消息给订阅者时,如果订阅者处于 未激 活状态状态:持久订阅者可以收到消息,而非持久订阅 者则收不到消息。

当然这种方式也有一定的影响:当持久订阅者处于 未激活 状态时,Broker需要为持久订阅者保存消息;如果持久订 阅者订阅的消息太多则会溢出

connection=connectionFactory.createConnectio
n(); 
connection.setClientID("Mic-001"); 
connection.start(); 
Session 
session=connection.createSession(Boolean.TRU
E,Session.AUTO_ACKNOWLEDGE); 
Topic 
destination=session.createTopic("myTopic"); 
MessageConsumer 
consumer=session.createDurableSubscriber(des
tination,"Mic-001"); 
TextMessage 
message=(TextMessage)consumer.receive(); 
System.out.println(message.getText());

持久订阅时,客户端向JMS 服务器注册一个自己身份的ID, 当这个客户端处于离线时,JMS Provider 会为这个ID 保 存所有发送到主题的消息,当客户再次连接到 JMS Provider时,会根据自己的ID得到所有当自己处于离线时 发送到主题的消息。 这个身份ID, 在代码中的体现就是connection的ClientID, 这个其实很好理解,你要想收到朋友发送的qq消息,前提 就是你得先注册个 QQ 号,而且还要有台能上网的设备, 电脑或手机。设备就相当于是clientId是唯一的;qq号相 当于是订阅者的名称,在同一台设备上,不能用同一个qq 号挂 2 个客户端。连接的 clientId 必须是唯一的,订阅者 的名称在同一个连接内必须唯一。这样才能唯一的确定连 接和订阅者。

JMS 消息的可靠性机制

理论上来说,我们需要保证消息中间件上的消息,只有被 消费者确认过以后才会被签收,相当于我们寄一个快递出 去,收件人没有收到快递,就认为这个包裹还是属于待签 收状态,这样才能保证包裹能够安全达到收件人手里。消 息中间件也是一样。 消息的消费通常包含 3 个阶段:客户接收消息、客户处理 消息、消息被确认 首先,来简单了解JMS的事务性会话和非事务性会话的概 念 JMS Session接口提供了commit和rollback方法。事务提 交意味着生产的所有消息被发送,消费的所有消息被确认; 事务回滚意味着生产的所有消息被销毁,消费的所有消息 被恢复并重新提交,除非它们已经过期。 事务性的会话总 是牵涉到事务处理中,commit 或 rollback 方法一旦被调 用,一个事务就结束了,而另一个事务被开始。关闭事务 性会话将回滚其中的事务 

在事务型会话中

在事务状态下进行发送操作,消息并未真正投递到中间件,而只有进行session.commit操作之后,消息才会发送到中间件,再转发到适当的消费者进行处理。如果是调用rollback操作,则表明,当前事务期间内所发送的消息都取消掉。通过在创建session的时候使用true or false来决定 当前的会话是事务性还是非事务性

connection.createSession(Boolean.TRUE,Session.AUTO_ ACKNOWLEDGE); 

在事务性会话中,消息的确认是自动进行,也就是通过 session.commit()以后,消息会自动确认。

➢ 必须保证发送端和接收端都是事务性会话 

在非事务型会话中
消息何时被确认取决于创建会话时的应答模式 (acknowledgement mode). 有三个可选项

Session.AUTO_ACKNOWLEDGE 

当客户成功的从 receive 方法返回的时候,或者从 MessageListenner.onMessage方法成功返回的时候,会话 自动确认客户收到消息。

Session.CLIENT_ACKNOWLEDGE 

客户通过调用消息的acknowledge方法确认消息。

CLIENT_ACKNOWLEDGE 特性 在这种模式中,确认是在会话层上进行,确认一个被消费 的消息将自动确认所有已被会话消费的消息。列如,如果 一个消息消费者消费了 10 个消息,然后确认了第 5 个消 息,那么0~5的消息都会被确认  ->

演示如下: 发送端发送10个消息,接收端接收10个消息, 但是在 i==5 的时候,调用 message.acknowledge()进行 确认,会发现0~4的消息都会被确认

Session.DUPS_ACKNOWLEDGE 

消息延迟确认。指定消息提供者在消息接收者没有确认发 送时重新发送消息,这种模式不在乎接受者收到重复的消 息。 

消息的持久化存储 

消息的持久化存储也是保证可靠性最重要的机制之一,也 就是消息发送到 Broker 上以后,如果 broker 出现故障宕 机了,那么存储在broker上的消息不应该丢失。可以通过 下面的代码来设置消息发送端的持久化和非持久化特性 

MessageProducer producer=session.createProducer(destination); 
producer.setDeliveryMode(DeliveryMode.PERSISTENT);

➢ 对于非持久的消息,JMS provider不会将它存到文件/数 据库等稳定的存储介质中。也就是说非持久消息驻留在 内存中,如果jms provider宕机,那么内存中的非持久 消息会丢失

➢ 对于持久消息,消息提供者会使用存储-转发机制,先将 消息存储到稳定介质中,等消息发送成功后再删除。如 果jms provider挂掉了,那么这些未送达的消息不会丢 失;jms provider 恢复正常后,会重新读取这些消息, 并传送给对应的消费者

 消息同步发送和异步发送

 

ActiveMQ支持同步、异步两种发送模式将消息发送到broker上。
同步发送过程中,发送者发送一条消息会阻塞直到broker反馈一个确认消息,表示消息已经被broker处理。这个机 制提供了消息的安全性保障,但是由于是阻塞的操作,会影响到客户端消息发送的性能
异步发送的过程中,发送者不需要等待broker提供反馈,所以性能相对较高。但是可能会出现消息丢失的情况。所 以使用异步发送的前提是在某些情况下允许出现数据丢失的情况。
默认情况下,非持久化消息是异步发送的,持久化消息并且是在非事务模式下是同步发送的。
但是在开启事务的情况下,消息都是异步发送。由于异步发送的效率会比同步发送性能更高。所以在发送持久化消 息的时候,尽量去开启事务会话。

除了持久化消息和非持久化消息的同步和异步特性以外,我们还可以通过以下几种方式来设置异步发送

 

1.ConnectionFactory connectionFactory=new ActiveMQConnectionFactory("tcp://192.168.11.153:61616?jms.useAsyncSend=true"); 
2.((ActiveMQConnectionFactory) connectionFactory).setUseAsyncSend(true); 
3.((ActiveMQConnection)connection).setUseAsyncSend(true);

 

 ProducerWindowSize的含义

producer每发送一个消息,统计一下发送的字节数,当字节数达到ProducerWindowSize值时,需要等待broker的 确认,才能继续发送。

代码在:ActiveMQSession的1957行

主要用来约束在异步发送时producer端允许积压的(尚未ACK)的消息的大小,且只对异步发送有意义。每次发送消 息之后,都将会导致memoryUsage大小增加(+message.size),当broker返回producerAck时,memoryUsage尺 寸减少(producerAck.size,此size表示先前发送消息的大小)。

可以通过如下2种方式设置:
Ø 在brokerUrl中设置: "tcp://localhost:61616?jms.producerWindowSize=1048576",这种设置将会对所有的 producer生效。

Ø 在destinationUri中设置: "test-queue?producer.windowSize=1048576",此参数只会对使用此Destination实例 的producer失效,将会覆盖brokerUrl中的producerWindowSize值。 注意:此值越大,意味着消耗Client端的内存就越大。

持久化在哪里?

ActiveMQ支持多种不同的持久化方式,主要有以下几种,不过,无论使用哪种持久化方式,消息的存储逻辑都是 一致的。

Ø KahaDB存储(默认存储方式)

Ø JDBC存储

Ø Memory存储

Ø LevelDB存储

Ø JDBC With ActiveMQ Journal

KahaDB存储
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。消息存储使用一个事务日志和仅仅用一个 索引文件来存储它所有的地址。
KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。在Kaha中,数据被追加到 data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。

<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配置

<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://192.168.11.156:3306/activemq? relaxAutoCommit=true"/>
  <property name="username" value="root"/>
  <property name="password" value="root"/>
</bean>

 

 LevelDB存储 

LevelDB持久化性能高于KahaDB,虽然目前默认的持久化方式仍然是KahaDB。并且,在ActiveMQ 5.9版本提供 了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。 不过,据ActiveMQ官网对LevelDB的表述:LevelDB官方不建议使用以及不再支持,推荐使用的是KahaDB

<persistenceAdapter>      
    <levelDBdirectory="activemq-data"/> 
</persistenceAdapter>

 

 JDBC Message store with ActiveMQ Journa

这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库和读库。 ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。
当消费者的消费速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。 举个例子,生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况 下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上的消息,那么这个时候只需要同步剩余的 10%的消息到DB。

如果消费者的消费速度很慢,这个时候journal文件可以使消息以批量方式写到DB。

Ø 将原来的标签注释掉
Ø 添加如下标签

 

 Ø 在服务端循环发送消息。可以看到数据是延迟同步到数据库的

posted @ 2020-01-08 12:49  MonsterZL  阅读(413)  评论(0编辑  收藏  举报