消息中间件--JMS--ActiveMQ--03

10 篇文章 0 订阅
10 篇文章 0 订阅

上接:消息中间件–JMS–ActiveMQ–02

消息中间件–JMS–ActiveMQ–03

9、ActiveMQ的传输协议

​ 前置知识:

服务器常用的几种IO模型:

​ Java对BIO、NIO、AIO的支持:

  • Java BIO (blocking I/O): 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
  • Java NIO (non-blocking I/O): 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
  • Java AIO(NIO.2) (Asynchronous I/O) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,

​ BIO、NIO、AIO适用场景分析:

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

​ 对于ActiveMQ而言,它采用的是Client–to–Broker的架构模式,服务器提供ip和端口供客户端访问,而ActiveMQ支持的网络传输协议有多种:TCP,NIO,UDP,SSL,Http(s),VM,amqp,stomp,mqtt,ws等。不同的协议有不同的特点。

​ 对于Java开发来讲,一般常用的是TCP和NIO,本文只介绍这两种的使用配置方式,其余的传输协议请参考官网。

https://activemq.apache.org/configuring-version-5-transports.html

​ 注意:TCP和NIO协议是支持传统JMS编程流程的,其他的协议有其他的客户端编程流程,不一定和上文中的编程方式一致。

​ ActiveMQ允许客户端使用多种协议来连接,配置Transport Connector的文件在activeMQ安装目录的conf/activemq.xml中的标签之内。官方默认提供的几项配置如下:

<transportConnectors>
	<!--都是固定的写法,不能自定义,name通常等于使用的协议名,但是因为tcp使用了名为openwire的wire protocol来将序列化的数据消息序列化成字节流,通过传输字节流促使网络上的效率和数据快速交互,所以tcp协议的name使用了openwire。其他的传输协议也由自己使用的wire protocol,但是他们没有使用此名称作为name-->
    <!--URI之前的tcp表示传输协议-->
    <transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
    <transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>

​ 从上面的配置可以看出,ActiveMQ启动之后,同时监听了多个端口,使用不同的传输协议来提供JMS服务。

9.1、TCP

​ 参考文档:

		https://activemq.apache.org/tcp-transport-reference

​ TCP传输协议是Broker的默认配置之一,采用BIO的同步阻塞IO模型,使用OpenWire序列化字节流,监听的端口号是61616。

​ 它的优点是:

​ 可靠性高,稳定性强

​ 高效性:字节流方式传递,效率很高

​ 有效性,可用性:应用广泛,支持任何平台

​ 缺点:

​ 基于BIO的IO模型,不适用于高并发的场景。

​ 服务器配置方式:

 <!--name为openwire,uri格式为:tcp://ip:port?k1=v1&amp;k2=v2&amp;k3=v3
	 传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="openwire" 
uri="tcp://localhost:61616?maximumConnections=1000
&amp;wireFormat.maxFrameSize=104857600"
&amp;transport.threadName
&amp;transport.trace=false
&amp;transport.soTimeout=60000/>

​ 客户端配置方式:

//格式:tcp://ip:port?k1=v1&k2=v2&k3=v3
//传输协议配置项前不加前缀,有限配置前加前缀
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(
"tcp://localhost:61616?transport.trace=false&wireFormat.cacheEnabled=false&wireFormat.tightEncodingEnabled=false"
);

​ 传输协议配置项(在服务器端配置时,必须使用transport.*作为前缀;在客户端配置时,不能使用前缀):

选项名称默认值描述
backlog5000指定传输服务器套接字等待接受的最大连接数。
closeAsynctrue如果**true套接字关闭调用是异步发生的。此参数应设置false**为STOMP等协议,这些协议通常用于为每次读取或写入创建新连接的情况。这样做可确保套接字关闭调用同步发生。同步关闭可防止代理由于连接的快速循环而耗尽可用套接字。
connectionTimeout30000如果**>=1该值设置连接超时(以毫秒为单位)。值为0**表示没有超时。负值被忽略。
daemonfalse如果**true传输线程将以守护进程模式运行。将此参数设置为true**将代理嵌入Spring容器或Web容器中以允许容器正确关闭。
dynamicManagementfalse如果**trueTransportLogger**可以通过JMX进行管理。
ioBufferSize8 * 1024指定在TCP层和**wireFormat**基于编组的OpenWire层之间使用的缓冲区的大小。
jmxPort1099(仅限客户端)指定JMX服务器将用于管理的端口**TransportLoggers**。这应仅由客户端生产者或消费者通过URI设置,因为代理创建自己的JMX服务器。指定备用JMX端口对于在同一台计算机上测试代理和客户端并且需要通过JMX控制这两者的开发人员非常有用。
keepAlivefalse如果**true在代理连接上启用TCP KeepAlive以防止连接在TCP级别超时。这应该与使用的KeepAliveInfo消息混淆InactivityMonitor。**
logWriterNamedefault设置**org.apache.activemq.transport.LogWriter要使用的实现的名称。名称映射到resources/META-INF/services/org/apache/activemq/transport/logwriters**目录中的类。
maximumConnectionsInteger.MAX_VALUE此代理允许的最大套接字数。
minmumWireFormatVersion0**wireFormat将被接受的最小远程版本(请注意拼写错误)。注意:当远程wireFormat版本低于配置的最低可接受版本时,将引发异常并且将拒绝连接尝试。值0表示不检查远程wireFormat**版本。
socketBufferSize64 * 1024设置接受的套接字读写缓冲区的大小(以字节为单位)。
soLingerInteger.MIN_VALUEsoLinger值为时设置套接字的选项> -1。当设置**-1soLinger**套接字选项被禁用。
soTimeout0设置套接字的读取超时(以毫秒为单位)。值为**0**表示没有超时。
soWriteTimeout0设置套接字的写入超时(以毫秒为单位)。如果套接字写操作未在指定的超时之前完成,则套接字将被关闭。值0表示没有超时。
stackSize0设置传输的后台读取线程的堆栈大小。必须以倍数指定**128K。值为0**表示忽略此参数。
startLoggingtrue如果传输堆栈**trueTransportLogger对象最初将消息写入日志。除非,否则忽略此参数trace=true**。
tcpNoDelayfalse如果设置**true**了套接字选项 TCP_NODELAY。这会禁用Nagle的小数据包传输算法。
threadNameN / A指定此参数时,将在调用传输期间修改线程的名称。附加远程地址,以便粘贴在传输方法中的调用将在线程名称中包含目标信息。当使用线程转储进行脱气时,这非常有用。
tracefalse导致通过传输发送的所有命令都被记录。要查看记录的输出,请定义**Log4j记录器:log4j.logger.org.apache.activemq.transport.TransportLogger=DEBUG**。
trafficClass0要在套接字上设置的流量类。
diffServ0(仅限客户端)要在传出数据包上设置的首选差分服务流量类,如RFC 2475中所述。有效整数值:[0,64]。有效的字符串值:EFAF[1-3][1-4]CS[0-7]。使用JDK 6时,仅在JVM使用IPv4堆栈时才有效。要使用IPv4堆栈,请设置系统属性**java.net.preferIPv4Stack=true。注意:同时指定’ diffServtypeOfService** ’ 是无效的,因为它们在TCP / IP包头中共享相同的位置
typeOfService0(仅限客户端)要在传出数据包上设置的首选服务类型值。有效的整数值:[0,256]。使用JDK 6时,仅在JVM配置为使用IPv4堆栈时才有效。要使用IPv4堆栈,请设置系统属性**java.net.preferIPv4Stack=true。注意:同时指定’ diffServtypeOfService** ’ 是无效的,因为它们在TCP / IP包头中共享相同的位置。
useInactivityMonitortrue当**falseInactivityMonitor**被禁用,连接永不超时。
useKeepAlivetrue在**true KeepAliveInfo空闲连接上发送消息时,防止其超时。如果此参数是false**连接,如果在指定的时间内没有在连接上收到任何数据,则连接仍将超时。
useLocalHostfalse当**true本地连接将使用值进行localhost的,而不是实际的本地主机名。在某些操作系统上,例如OS X,无法以本地主机名连接,因此localhost**更好。
useQueueForAccepttrue当**true**接受的套接字被放置到队列上以使用单独的线程进行异步处理时。
wireFormatdefault**wireFormat**要使用的工厂的名称。
wireFormat。*N / A具有此前缀的属性用于配置**wireFormat**。

​ OpenWire Wire Format配置项:

​ OpenWire是ActiveMQ使用的默认有线格式。它为高速消息传递提供了高效的二进制格式。可以在JMS客户端的连接URI或代理的传输绑定URI上配置OpenWire选项。

​ 注意,与传输协议的配置方式不同,无论是在在服务器端配置还是在客户端配置,都必须使用 wireFormat. *作为前缀,否则配置不生效。

选项默认描述
cacheEnabledtrue是否应该缓存通常重复的值,以便减少编组?
cacheSize1024cacheEnabled=true,则此参数用于指定值的数量被缓存。
maxInactivityDuration30000最大不活动持续时间(在套接字被认为死亡之前),以毫秒为单位。在某些平台上,套接字可能需要很长时间才能消亡。因此,允许代理在配置的时间段内处于非活动状态时终止连接。某些传输使用它来启用保持心跳功能。设置为值时禁用不活动监视<= 0
maxInactivityDurationInitalDelay10000开始不活动检查之前的初始延迟。是的,这个词 'Inital'应该像那样拼写错误。
maxFrameSizeMAX_LONG允许的最大帧大小。可以帮助防止OOM DOS攻击。
sizePrefixDisabledfalse在每个数据包被封送之前,是否应该为数据包的大小添加前缀?
stackTraceEnabledtrue是否应将代理上发生的异常堆栈跟踪发送到客户端?
tcpNoDelayEnabledtrue不影响有线格式,但提供TCP_NODELAY应在通信套接字上启用的对等体的提示 。
tightEncodingEnabledtrue电线尺寸是否应优于CPU使用

9.2、TCP增强–NIO

​ 参考文档:

		https://activemq.apache.org/nio-transport-reference

​ NIO传输协议是对TCP协议的加强,底层依然是TCP协议,只不过采用了NIO(异步阻塞)的IO模型,与BIO不同的是,所有的请求共享一个线程池,而不是一个请求一个线程,节省了服务器的资源,能够支持更高的并发量。除了更换了IO模型,其余的都和TCP相同,比如wire protocol,还是OpenWire。可以将NIO协议视为TCP协议的NIO版本。

​ NIO传输协议不是ActiveMQ的默认配置项,如果需要使用此协议,需要在配置文件中进行配置。

​ 服务器配置方式(与tcp的配置方式相比,只是更换了name和协议名,配置项的配置和tcp相同):

<!--name为nio,uri格式为:nio://ip:port?k1=v1&amp;k2=v2&amp;k3=v3
	 传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="nio" 
uri="nio://localhost:61617?maximumConnections=1000
&amp;wireFormat.maxFrameSize=104857600"
&amp;transport.threadName
&amp;transport.trace=false
&amp;transport.soTimeout=60000
&amp;org.apache.activemq.transport.nio.SelectorManager.corePoolSize/>

​ 客户端配置方式(与tcp相比,仅仅更换了协议名称,其余不变):

//格式:tcp://ip:port?k1=v1&k2=v2&k3=v3
//传输协议配置项前不加前缀,有线配置前加前缀
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(
"nio://localhost:61617?transport.trace=false&wireFormat.cacheEnabled=false&wireFormat.tightEncodingEnabled=false"
);

​ 使用NIO模型之后,我们可以在URI后配置一些NIO的属性,例如使用以下系统属性调整传输使用的线程数(自5.15.0可用

属性默认值描述
org.apache.activemq.transport.nio.SelectorManager.corePoolSize10即使它们处于空闲状态,也要保留在池中的线程数
org.apache.activemq.transport.nio.SelectorManager.maximumPoolSize1024池中允许的最大线程数
org.apache.activemq.transport.nio.SelectorManager.workQueueCapacity0在增长池之前的最大工作队列深度
org.apache.activemq.transport.nio.SelectorManager.rejectWorkfalse当达到容量时,允许使用IOException拒绝工作,以便可以保留现有的QOS

9.3、其他协议配置NIO模型

​ 在ActiveMQ中,除了TCP以外,amqp,stomp,mqtt在默认情况下使用的也是BIO模型,不适用于高并发场景。9.2中介绍的NIO协议是对TCP的增强,将原来TCP协议使用的BIO模型更换为了NIO模型,提高了并发性能。

​ 那么,我们能不能将amqp,stomp,mqtt,ws等的IO模型也更改为NIO模型呢?答案是肯定的,我们可以通过如下的配置实现这一效果:

​ 第一种:针对具体协议进行配置(以amqp为例):

​ 服务端配置:

 <!--name为amqp+nio,uri格式为:amqp+nio://ip:port?k1=v1&amp;k2=v2&amp;k3=v3
	 传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="amqp+nio" 
uri="amqp+nio://localhost:61617?maximumConnections=1000
&amp;wireFormat.maxFrameSize=104857600"
&amp;transport.threadName
&amp;transport.trace=false
&amp;transport.soTimeout=60000/>

​ 客户端配置:

mqtt+nio://localhost:61617?transport.trace=false&wireFormat.cacheEnabled=false&wireFormat.tightEncodingEnabled=false"

​ 第二种:使用auto,让ActiveMQ自动识别传输协议

​ 在上述的所有配置中,都是一个端口对应一个传输协议,如果使用了auto,就相当于统一设置了一个端口,同时支持TCP, NIO,STOMP, AMQP, and MQTT协议,并且使用NIO模型。

​ 服务端配置:

 <!--name为amqp+nio,uri格式为:amqp+nio://ip:port?k1=v1&amp;k2=v2&amp;k3=v3
	 传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="auto+nio" 
uri="auto+nio://localhost:61618?maximumConnections=1000
&amp;wireFormat.maxFrameSize=104857600"
&amp;transport.threadName
&amp;transport.trace=false
&amp;transport.soTimeout=60000/>

​ 客户端配置:

//注意:此处不能使用auto关键字,因为客户端必须指定使用的传输协议,使用TCP, NIO,STOMP, AMQP, MQTT均可
//但是在编写客户端代码时,STOMP, AMQP, MQTT这几种协议的编码方式和TCP, NIO不同,如果想要使用这几种传输协议,请另行学习,本文不作说明。
nio://localhost:61618?transport.trace=false&wireFormat.cacheEnabled=false&wireFormat.tightEncodingEnabled=false"

10、ActiveMQ的持久化机制

​ 所有的MQ服务器都会面临一个问题,那就是一旦机器故障,接收的所有消息都会丢失,这是非常可怕的事情。为了解决这个问题,一般的消息服务器都会采用持久化机制,将接收到的消息持久化保存起来。

​ 总的过程是这样的:

​ MQ服务器接收到生产者发送的消息后,首先将消息持久化储存到本地数据文件、内存数据库或者远程数据库中,然后再试图将消息发给消费者,消费者签收之后则将消息从存储中删除,失败则继续尝试发送。

​ 采用了这种机制之后,即使MQ服务器挂了,消息也不会丢失,重启机器之后,MQ服务器会从存储位置检测是否有未发送的消息,如果有,则会取出消息,然后进行发送。

​ 对于ActiveMQ来讲,它的持久化机制有AMQ、KahaDB、LevelDB和JDBC,无论哪一种持久化机制,消息的存储逻辑都是一样的,只不过存储的位置和保存的形式有差别。具体可参见官方文档:

​ http://activemq.apache.org/persistence

​ 下面我们逐个介绍各自的使用方式:

10.1、AMQ

​ AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特点。消息存储在一个个文件中,文件的默认大小为32M,当一个文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。

​ AMQ适用于ActiveMQ5.3之前的版本,现在已经被其他更好的机制代替了,很少被使用到了,所以做一个简单了解即可。

10.2、KahaDB

​ KahaDB是内嵌在ActiveMQ中的一种数据库,是基于文件的本地数据库存储形式,它的位置在ActiveMQ安装目录/data/kahadb,在其目录中有四类文件和一个lock,如下:
在这里插入图片描述

​ db-.log:此文件用来存储消息,当存储到预定义大小时就会创建一个新的文件,number的数值也会随之递增。当一个文件的消息都被消费完毕时,文件就会被删除(点对点)或者归档(订阅/发布)。

​ db.data:该文件包含了持久化的BTree索引,索引了db-.log中的消息,类似于字典的目录。

​ db.free:记录当前db.data文件里哪些页面是空闲的,文件具体内容是所有空闲页的ID。

​ db.redo:用来进行消息恢复的,如果KahaDB被强制退出了,重启后用于恢复BTree索引,算是数据备份。

​ lock:文件锁,用于控制broker的读写权限,代表当前拥有读写权限的Broker。

​ KahaDB是ActiveMQ自ActiveMQ5.3版本以来的默认持久化方式,可用于任何场景,相比于AMQ提高了性能和恢复能力。

​ 它的存储原理很简单,就是通过BTree完成消息的索引,然后对消息进行读写。

​ 配置方式:

​ 在ActiveMQ配置文件中有以下配置,就是对KahaDB的默认配置:
在这里插入图片描述

​ 如果需要查看或者更改默认的参数配置,可参见官方文档:

​ http://activemq.apache.org/kahadb.html

10.3、LevelDB

​ 官方文档:

	http://activemq.apache.org/leveldb-store

​ 这种文件系统是从ActiveMQ5.8之后引进的,它和KahaDB非常相似,也是基于文件的本地数据库存储形式,但是它提供比KahaDB更快的持久性。但它不使用自定义B-Tree实现来索引独写日志,而是使用基于LevelDB的索引。

​ 默认配置如下:

 <broker brokerName="broker" ... >
    ...
    <persistenceAdapter>
      <levelDB directory="activemq-data"/>
    </persistenceAdapter>
    ...
  </broker>

​ 查看或者更改默认的参数配置可参见官方文档。

​ 但是这种持久化方式目前还没有被广泛使用,在将来可能会成为很不错的选择,并且它还有加强版本:

Replicated-leveldb-store,可复制的leveldb存储。

10.4、JDBC

​ 这种持久化机制是将MQ和关系型数据库进行连接,当MQ收到消息后,将消息存储到数据库表中完成持久化操作。和上述三种方式相比,它是一种非本地化的操作,MQ和数据库分别部署在不同的机器上,实现了物理分离的备份,可以更好的容灾。

​ 但是,因为要涉及到数据库操作,所以它的效率不如上面三种本地化的持久化机制,不过我们可以配合ActiveMQ Journal使用,来提高它的效率。
在这里插入图片描述

​ 下面我们分两种情况演示如何配置JDBC持久化:

10.4.1、JDBC Message Store

​ 这种方式是直接将MQ和数据库(这里使用Mysql)做连接,中间没有其他组件。它的原理是这样的:

​ 当MQ服务器启动时,连接到预先配置的数据库中,在数据库中创建表,并且设置表中的字段,当MQ服务区接收到消息后,首先将消息存储到表中,然后尝试向消费者发送消息,发送成功后删除消息或者标记已消费,发送失败则尝试重新发送。

​ 第一步:导入Mysql驱动包

​ 因为要设计到JDBC操作,所以必须导入Mysql驱动包,导入的位置在ActiveMQ安装目录/lib,该目录防止了ActiveMQ运行时依赖的第三方jar包,默认情况下已经存在以下依赖包:
在这里插入图片描述
​ 至于如何下载驱动jar包和上传jar就不做演示了。

​ 第二步:修改activemq.xml配置文件

​ 配置数据库连接池(位置在文件最底部,和之间):

<!--activemq.xml本质上就是一个Spring的配置文件,类似于在Spring中配置一个bean,默认采用dbcp连接池,如果需要使用其他连接池,需要导入jar包依赖,方法和第一步相同-->
<!--注意,低版本第ActiveMQ使用的是dbcp,而不是dbcp2,我是用的5.19.5-->
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> 
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> 
    <!--注意,此处是远程连接Mysql服务器,需要开启Mysql的远程连接,详情看第三步
		另外,在配置中要增加relaxAutoCommit=true-->
    <property name="url" value="jdbc:mysql://192.168.1.5:3306/activemq?relaxAutoCommit=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;userSSL=false&amp;serverTimezone=GMT%2B8"/> 
    <property name="username" value="root"/> 
    <property name="password" value="1234"/> 
    <property name="poolPreparedStatements" value="true"/> 
</bean>

​ 将原来KahaDB的配置注掉,在其下加入新的配置:

<persistenceAdapter> 
    <!--#mysql-ds表示引用上面配置的连接池bean
		createTablesOnStartup表示是否在MQ启动的时候在数据库中创建表,默认为true,如果不是第一次启动,请设置为false,防止重复建表-->
	<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true"/> 
</persistenceAdapter>

​ 第三步:开启Mysql的远程连接功能

​ 修改用户远程能访问,主要就是修改mysql数据库下user用户表中的host字段,% 就表示所有网络都可以访问,也就是外网能够访问。

	use mysql;
	update user set host="%" where user="root";
	flush privileges;

​ 第四步:在数据库中创建名为activemq的仓库,与上面的配置一致。

​ 只需要创建仓库,不需要创建表,并且仓库必须在启动ActiveMQ之前创建,否则找不到数据库会报错。

​ 第五步:启动ActiveMQ服务器,通过网页访问ActiveMQ的8161端口

​ 这一步非常重要,因为上面在配置jdbc时,容易出现错误,通过访问8161端口,可以检查ActiveMQ是否正常启动,启动成功表示配置没问题。

​ 第六步:查看activemq数据库是否自动生成了一下三张表:
在这里插入图片描述
​ 一般情况下都是自动生成的,如果第五步正常,但是没有自动生产,可以自己创建,将下面的sql在activemq数据库下执行即可:

-- auto-generated definition
create table ACTIVEMQ_ACKS
(
    CONTAINER     varchar(250)     not null comment '消息的Destination',
    SUB_DEST      varchar(250)     null comment '如果使用的是Static集群,这个字段会有集群其他系统的信息',
    CLIENT_ID     varchar(250)     not null comment '每个订阅者都必须有一个唯一的客户端ID用以区分',
    SUB_NAME      varchar(250)     not null comment '订阅者名称',
    SELECTOR      varchar(250)     null comment '选择器,可以选择只消费满足条件的消息,条件可以用自定义属性实现,可支持多属性AND和OR操作',
    LAST_ACKED_ID bigint           null comment '记录消费过消息的ID',
    PRIORITY      bigint default 5 not null comment '优先级,默认5',
    XID           varchar(250)     null,
    primary key (CONTAINER, CLIENT_ID, SUB_NAME, PRIORITY)
)
    comment '用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存';

create index ACTIVEMQ_ACKS_XIDX
    on ACTIVEMQ_ACKS (XID);
 
-- auto-generated definition
create table ACTIVEMQ_LOCK
(
    ID          bigint       not null
        primary key,
    TIME        bigint       null,
    BROKER_NAME varchar(250) null
);

-- auto-generated definition
create table ACTIVEMQ_MSGS
(
    ID         bigint       not null
        primary key,
    CONTAINER  varchar(250) not null,
    MSGID_PROD varchar(250) null,
    MSGID_SEQ  bigint       null,
    EXPIRATION bigint       null,
    MSG        blob         null,
    PRIORITY   bigint       null,
    XID        varchar(250) null
);

create index ACTIVEMQ_MSGS_CIDX
    on ACTIVEMQ_MSGS (CONTAINER);

create index ACTIVEMQ_MSGS_EIDX
    on ACTIVEMQ_MSGS (EXPIRATION);

create index ACTIVEMQ_MSGS_MIDX
    on ACTIVEMQ_MSGS (MSGID_PROD, MSGID_SEQ);

create index ACTIVEMQ_MSGS_PIDX
    on ACTIVEMQ_MSGS (PRIORITY);

create index ACTIVEMQ_MSGS_XIDX
    on ACTIVEMQ_MSGS (XID);

​ 三张表的作用详解如下:

​ activemq_msgs:用来持久化储存消息,包括Queue和Topic消息

ID自增的数据库主键,表示每条消息进入表的顺序,从1开始
CONTAINER消息的Destination
MSGID_PROD消息发送者的唯一标识
MSG_SEQ指发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID
EXPIRATION消息的过期时间,默认为0,永不过期
MSG消息本体的Java序列化对象的二进制数据
PRIORITY优先级,从0-9,数值越大优先级越高
XID

​ activemq_acks:用于储存订阅关系。如果是持久化订阅,订阅者和服务器的订阅关系会在这个表里保存。

CONTAINER订阅的Destination
SUB_DEST如果是使用Static集群,这个字段会有集群中其他系统的信息
CLIENT_ID订阅者的唯一ID
SUB_NAME订阅者的名称
SELECTOR选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,支持and和or
LAST_ACKED_ID记录最后一个被消费的消息ID
PRIORITY优先级,从0-9,数值越大优先级越高
XID

​ activemq_lock:在集群环境中才有用,只有一个Broker可以获得消息,成为Master Broker,其他的Broker只能作为备份,如果Master Broker不可用,它们采用可能成为Master Broker。这个表用于记录那个Broker是当前的Master Broker。

ID当前Master Broker的唯一标识
Broker Name拥有lock的ActiveMQ Broker Name

​ 第七步:通过以上六步,ActiveMQ和Mysql数据库的整合已经完成,下面就可以进行测试了。

​ 此处不再展示Queue和Topic的生产者消费者代码,可以参考上文,需要注意的一点就是:消息生产者必须开启消息的可持久化。

​ 此时Queue的生产者向MQ发送消息后,MQ会首先将受到的消息保存在activemq_msgs,然后再发送给消费者,消费者签收消息之后,MQ会将activemq_msgs中的消息删除。

​ Topic的持久订阅者启动后,MQ会将订阅关系保存到activemq_acks中,当Topic的生产者向MQ发送消息后,MQ会将消息保存在activemq_msgs中,然后向订阅者推送,订阅者签收之后,activemq_msgs中的Topic消息不会被删除,但是activemq_acks中的LAST_ACKED_ID字段会记录最后一个被消费的ID,用来区别未消费的消息。

10.4.2、JDBC Message Store with ActiveMQ Journal

​ 上面介绍的JDBC Message Store是ActiveMQ和数据库采用直连的方式完成消息的持久化,这样虽然可以实现非本地化的持久化操作,提高了系统的容灾能力,但是因为要涉及到网络传输,频繁的对数据库执行读写操作,效率是很低下的。为了解决这个问题,ActiveMQ为我们提供了ActiveMQ Journal,我们可以将它和JDBC操作配合使用,提高持久化的效率。

​ 它的原理是这样的:

​ ActiveMQ Journal是一种高速缓存的技术,它处在ActiveMQ服务器和数据库之间,当MQ收到消息之后,它将消息持久化储存到Journal中,而不是直接面向数据库,当消费者的速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。只有当消费者的速度跟不上生产者的速度时,才将journal中的消息持久化到数据库中。

​ 举个例子:
​ 生产者生产了1000条消息,这1000条消息会马上保存到journal文件,如果消费者的消费速度很快的情况下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上消息,那么这个时候只需要同步剩余的10%的消息到DB。如果消费者的速度很慢,这个时候journal文件可以使消息以批量方式写到DB。

​ 归根接地,Journal在ActiveMQ服务器和数据库之间起到了一个缓存的作用,减少了IO次数,提高了效率。

​ ActiveMQ Journal的使用方式很简单,只是将JDBC Message Store配置第二步的persistenceAdapter配置部分更换成了下面的配置:

<persistenceFactory>        
    <journalPersistenceAdapterFactory 
        journalLogFiles="5" 
        journalLogFileSize="32768" 
        useJournal="true" 
        useQuickJournal="true" 
        <!--下游的数据源-->                              
        dataSource="#mysql-ds" 
        <!--journal的存储位置-->
        dataDirectory="../activemq-data" /> 
</persistenceFactory>

​ 改完配置之后,需要重新启动ActiveMQ,然后就可以看到在ActiveMQ的安装目录下多了一个activemq-data文件夹,里面的内容如下:
在这里插入图片描述
​ journal缓存的消息就是存放在log-.dat中的。

​ 注意:Journal只能缓存消息,持久化订阅的信息还是会被立刻储存到数据库activemq_acks表中,一段时间之后,Journal根据消息的消费情况,更新activemq_acks中的LAST_ACKED_ID字段值,没有更新的消息持久化存储到activemq_msgs表中。

11、ActiveMQ的集群搭建

​ 官方文档:http://activemq.apache.org/replicated-leveldb-store

​ 常用的有Zookeeper + Replicated LevelDB store和JDBC Master Slave,详情暂缺。

12、ActiveMQ的高级特性

12.1、异步投递

​ 参考文档:http://activemq.apache.org/async-sends

​ 在ActiveMQ中,它支持生产者采用同步或者异步的方式向MQ服务器发送消息。同步模式下,只有在MQ服务器持久化完成,并返回确认结果后,生产者的发送线程才会被释放;异步模式下,生产者发送消息之后,线程立即释放,不用等待MQ服务器的确认结果。两者比较而言,异步的发送效率更高,但是可能会产生消息丢失的情况,如果能够忍受少量消息丢失,并且想提高生产效率的话,可以采用异步发送的方式。

​ 默认情况下,除了不开启事务发送持久化消息之外,其余情况都是异步发送消息的。如果我们需要自定义设置,有一下三种方式:

//使用连接URI配置异步发送:
cf = new ActiveMQConnectionFactory("tcp://locahost:61616?jms.useAsyncSend=true");

//在ConnectionFactory级别配置异步发送
((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true);

//在连接级别配置异步发送
((ActiveMQConnection)connection).setUseAsyncSend(true);

​ 异步发送消息存在这样一个问题,因为生产者发送消息之后是不关系MQ服务器是否已经持久化储存了消息的,如果消息发送之后,MQ服务器宕机了,就会出现消息丢失的情况,那么异步消息如何确定发送成功呢?

​ 答案是:在生产者发送消息时设置异步回调函数!!!

package com.demo;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.AsyncCallback;

import javax.jms.*;
import java.util.UUID;

public class Producer {
    private static final String ACTIVEMQ_URL = "tcp://192.168.10.130:61616";
    private static final String ACTIVEMQ_QUEUE_NAME = "Queue-异步投递回调";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory();
        activeMQConnectionFactory.setBrokerURL(ACTIVEMQ_URL);
        //开启异步投递
        activeMQConnectionFactory.setUseAsyncSend(true);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
        //向上转型到ActiveMQMessageProducer
        ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
        for (int i = 0; i < 3; i++) {
            TextMessage textMessage = session.createTextMessage("message-" + i);
            textMessage.setJMSMessageID(UUID.randomUUID().toString() + "----orderAtguigu");
            String textMessageId = textMessage.getJMSMessageID();
            //使用ActiveMQMessageProducer的发送消息,可以创建回调
            //MessageProducer是不能设置回调函数的,所以使用JmsTemplate也是不能够设置的
            activeMQMessageProducer.send(textMessage, new AsyncCallback() {
                @Override
                public void onSuccess() {
                    System.out.println(textMessageId + "发送成功");
                }
				
                //失败后的处理方式,可以做日志记录、重新发送等操作。
                @Override
                public void onException(JMSException exception) {
                    System.out.println(textMessageId + "发送失败");
                }
            });
        }
        activeMQMessageProducer.close();
        session.close();
        connection.close();
    }
}

12.2、延时和定时投递

​ 参考文档:http://activemq.apache.org/delay-and-schedule-message-delivery.html

​ 自ActiveMQ5.4版本之后,ActiveMQ支持了计划投递功能,我们可以通过在配置文件中开启 broker schedulerSupport来使用这样功能。
在这里插入图片描述
​ 开启之后,我们就可以在Message中设置一些消息属性,消息属性的key从ScheduledMessage常量接口获取,value可以根据实际情况进行设置。Message设置好之后就可以发送给MQ服务器,MQ会根据我们的设定向消费者有计划地发送消息。

package com.demo;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.AsyncCallback;
import org.apache.activemq.ScheduledMessage;
import org.springframework.scheduling.annotation.Scheduled;

import javax.jms.*;
import java.util.UUID;

/**
 * 计划投递
 */
public class Producer_延迟投递 {
    private static final String ACTIVEMQ_URL = "tcp://192.168.10.130:61616";
    private static final String ACTIVEMQ_QUEUE_NAME = "Queue-计划投递";

    public static void main(String[] args) throws JMSException {
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory();
        activeMQConnectionFactory.setBrokerURL(ACTIVEMQ_URL);
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
        //向上转型到ActiveMQMessageProducer
        MessageProducer messageProducer = session.createProducer(queue);
        long delay = 3 * 1000;      //延迟投递的时间
        long period = 4 * 1000;     //每次投递的时间间隔
        int repeat = 5;             //投递的次数

        for (int i = 0; i < 3; i++) {
            TextMessage textMessage = session.createTextMessage("message-延时投递" + i);
            //给消息设置属性以便MQ服务器读取到这些信息,好做对应的处理
            textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
            textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
            textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
            messageProducer.send(textMessage);
        }
        messageProducer.close();
        session.close();
        connection.close();
    }
}

​ 计划投递的常用属性:

Property nametypedescription
AMQ_SCHEDULED_DELAYlongThe time in milliseconds that a message will wait before being scheduled to be delivered by the broker
AMQ_SCHEDULED_PERIODlongThe time in milliseconds to wait after the start time to wait before scheduling the message again
AMQ_SCHEDULED_REPEATintThe number of times to repeat scheduling a message for delivery
AMQ_SCHEDULED_CRONStringUse a Cron entry to set the schedule

12.3、消息重试机制

​ 参考文档:http://activemq.apache.org/redelivery-policy

​ 正常情况下,每一条消息都会被签收,签收之后的消息不会被重复发送,这是ActiveMQ保证消息不被重复发送的机制。但是,如果消息首次发送之后,消费端没有正常签收,或者在CLIENT_ACKNOWLEDGE模式下调用了recover(重发)方法,也就是说,消息没有被消费者正常处理,需要MQ服务器重新发送消息。

​ 为了保证每一条消息都能够被正常消费,ActiveMQ设立了消息重试机制,允许消息消费失败后重新发送消息。但是这种重复发送也不是没有限制的,默认情况下是发送失败后间隔1s,最高重发6次,当一条消息的重发次数达到6次之后,消费者就会向MQ返回“posion ack”,表示此消息是有毒消息,无法被处理,不要再重复发送了。MQ接收到“posion ack”之后,就会把该条消息存放到死信队列(DLQ:death letter queue)中,不再重复发送。

​ 关于消息重试机制,可以设置的属性有以下几个:

PropertyDefault ValueDescription
initialRedeliveryDelay1000L首次重发等待时间
redeliveryDelay1000L重发等待时间,默认为1s,initialRedeliveryDelay=0时才有效。一般使用initialRedeliveryDelay,两者等价
useExponentialBackOfffalse是否指数性的增长重发等待时间
backOffMultiplier5重发等待时间每次增长的倍数
maximumRedeliveries6最大的重发次数
maximumRedeliveryDelay-1最大的重发等待时间。-1表示无限制
useCollisionAvoidancefalseShould the redelivery policy use collision avoidance.
collisionAvoidanceFactor0.15The percentage of range of collision avoidance if enabled.

​ 这些属性的设置需要在RedeliveryPolicy类中进行,然后将RedeliveryPolicy设置到ActiveMQConnection或者ActiveMQConnectionFactory中:

ActiveMQConnection connection ...  // Create a connection

RedeliveryPolicy queuePolicy = new RedeliveryPolicy();
queuePolicy.setInitialRedeliveryDelay(0);
queuePolicy.setRedeliveryDelay(1000);
queuePolicy.setUseExponentialBackOff(false);
queuePolicy.setMaximumRedeliveries(2);

RedeliveryPolicy topicPolicy = new RedeliveryPolicy();
topicPolicy.setInitialRedeliveryDelay(0);
topicPolicy.setRedeliveryDelay(1000);
topicPolicy.setUseExponentialBackOff(false);
topicPolicy.setMaximumRedeliveries(3);

// Receive a message with the JMS API
RedeliveryPolicyMap map = connection.getRedeliveryPolicyMap();
// ">"表示所有
map.put(new ActiveMQTopic(">"), topicPolicy);
map.put(new ActiveMQQueue(">"), queuePolicy);

connection.setRedeliveryPolicyMap(map);

​ 或者也可以在 Connection Configuration URI 中再地址后拼接参数进行设置,此处不再演示。

12.4、死信队列

​ 参考文档:http://activemq.apache.org/message-redelivery-and-dlq-handling.html

​ 上面有提到,当消息重发次数超过上限时,没有消费的消息都会进入死信队列,这里的消息不会被MQ再次发送,开发人员可以在这里检查出错的消息,进行人工干预。

​ 每一个destination产生的死信如何处理是由该destination的死信处理策略决定的,ActiveMQ提供两种死信处理策略,一种是共享私信策略,一种是独立私信策略。

​ 默认情况下,broker采用共享的死信策略,所有的destination产生的死信都进入统一的死信队列中,名为“ActiveMQ.DLQ”,如果需要修改默认的共享策略配置,可以在配置文件中进行下面的配置:

<broker>
  
  <destinationPolicy>
   <policyMap>
     <policyEntries>
<!------------------------------------------------------------------------------------->
       <!-- 更改所有Queue的默认共享策略 -->
       <!-- “>”表示所有,此项配置对所有的queue都生效 -->
       <policyEntry queue=">" >
         <deadLetterStrategy>
           <!---deadLetterQueue,为共享死信队列的名称,默认的名称为ActiveMQ.DLQ
				processExpired:是否将过期的死信放置到死信队列中,默认为true
				processNonPersistent:是否将非持久化的死信放置到死信队列中,默认为false
				expiration:死信队列中消息的过期时间(ms),默认永不过期,过期会被删除
				useQueueForQueueMessage,用在Queue死信配置里,表示是否使用Queue形式的死信队列,默认为true-->
           <sharedDeadLetterStrategy deadLetterQueue="xxx" processExpired="false" processNonPersistent="true" expiration="300000" useQueueForQueueMessage="false"/>
         </deadLetterStrategy>
       </policyEntry>
<!------------------------------------------------------------------------------------->
       <!-- 更改所有Topic的默认共享策略 -->
       <policyEntry topic=">" >
         <deadLetterStrategy>
           <sharedDeadLetterStrategy deadLetterQueue="xxx" processExpired="false" processNonPersistent="true" expiration="300000" useQueueForTopicMessage="false"/>
         </deadLetterStrategy>
       </policyEntry>
<!------------------------------------------------------------------------------------->
     </policyEntries>
   </policyMap>
  </destinationPolicy>
  
</broker>

​ 除了默认的共享死信策略,ActiveMQ还提供了独立的私信策略,支持将每一个destination产生的死信放置到各自的死信队列中。没有进行独立死信策略配置的destination,均采用默认的私信策略。

<broker>
  
  <destinationPolicy>
   <policyMap>
     <policyEntries>
<!------------------------------------------------------------------------------------->
       <!-- 对单独的Queue设置独立的死信队列 -->
       <policyEntry queue="queue01" >
         <deadLetterStrategy>
           <!-- queuePrefix表示死信队列名字的前缀,用在Queue死信配置里,默认为ActiveMQ.DLQ.Queue.
 				后缀为队列的名称,比如当前设置的死信队列名为DLQ.queue01
				useQueueForQueueMessage,用在Queue死信配置里,表示是否使用Queue形式的死信队列,默认为true-->
           <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessage="false" processExpired="false" processNonPersistent="true" expiration="300000"/>
         </deadLetterStrategy>
       </policyEntry>
<!------------------------------------------------------------------------------------->
       <!-- 对单独的Topic设置独立的死信队列 -->
       <policyEntry topic="topic01" >
         <deadLetterStrategy>
           <!-- topicPrefix表示死信队列名字的前缀,用在Topic死信配置里,默认为ActiveMQ.DLQ.Topic.
 				后缀为主体的名称,比如当前设置的死信队列名为DLQ.queue01
				useQueueForTopicMessage,用在Topic死信配置里,表示是否使用Queue形式的死信队列,默认为true-->
           <individualDeadLetterStrategy topicPrefix="DLQ." useQueueForTopicMessage="true" processExpired="false" processNonPersistent="true" expiration="300000"/>
         </deadLetterStrategy>
       </policyEntry>
<!------------------------------------------------------------------------------------->
       <!-- 对所有的Queue设置独立的死信队列 -->
       <policyEntry queue=">" >
         <deadLetterStrategy>
           <individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessage="false" processExpired="false" processNonPersistent="true" expiration="300000"/>
         </deadLetterStrategy>
       </policyEntry>
<!------------------------------------------------------------------------------------->
       <!-- 对所有的Topic设置独立的死信队列 -->
       <policyEntry topic=">" >
         <deadLetterStrategy>
           <individualDeadLetterStrategy topicPrefix="DLQ." useQueueForTopicMessage="true" processExpired="false" processNonPersistent="true" expiration="300000"/>
         </deadLetterStrategy>
       </policyEntry>
<!------------------------------------------------------------------------------------->
     </policyEntries>
   </policyMap>
  </destinationPolicy>
  
</broker>

12.5、消息的幂等性问题

​ 消息的幂等性问题指的是:当一个消息被正常消费之后,消费者在向服务器发送签收信息时,如果发生了网络波动,此时可能会触发消息的重复发送,造成信息的重复消费。

​ 为了解决这种问题,我们可以利用关系型数据库的主键唯一性或者非关系型数据库的key唯一性来解决:

​ 因为每一条消息都有唯一的MessageID,在消费者消费一条消息之前,我们先获取该条消息的MessageID,然后在数据库中查询是否存在此ID,如果存在,代表已经消费过,不再重新消费;如果不存在,则正常消费。

​ 也就是说,在消费信息之前做一个逻辑判断,符合条件的消费,不符合条件的不消费。

​ 建议使用redis,因为Nosql的查询效率比较高。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值