上接:消息中间件–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&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&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&k2=v2&k3=v3
传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="openwire"
uri="tcp://localhost:61616?maximumConnections=1000
&wireFormat.maxFrameSize=104857600"
&transport.threadName
&transport.trace=false
&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.*作为前缀;在客户端配置时,不能使用前缀):
选项名称 | 默认值 | 描述 |
---|---|---|
backlog | 5000 | 指定传输服务器套接字等待接受的最大连接数。 |
closeAsync | true | 如果**true 套接字关闭调用是异步发生的。此参数应设置false **为STOMP等协议,这些协议通常用于为每次读取或写入创建新连接的情况。这样做可确保套接字关闭调用同步发生。同步关闭可防止代理由于连接的快速循环而耗尽可用套接字。 |
connectionTimeout | 30000 | 如果**>=1 该值设置连接超时(以毫秒为单位)。值为0 **表示没有超时。负值被忽略。 |
daemon | false | 如果**true 传输线程将以守护进程模式运行。将此参数设置为true **将代理嵌入Spring容器或Web容器中以允许容器正确关闭。 |
dynamicManagement | false | 如果**true 在TransportLogger **可以通过JMX进行管理。 |
ioBufferSize | 8 * 1024 | 指定在TCP层和**wireFormat **基于编组的OpenWire层之间使用的缓冲区的大小。 |
jmxPort | 1099 | (仅限客户端)指定JMX服务器将用于管理的端口**TransportLoggers **。这应仅由客户端生产者或消费者通过URI设置,因为代理创建自己的JMX服务器。指定备用JMX端口对于在同一台计算机上测试代理和客户端并且需要通过JMX控制这两者的开发人员非常有用。 |
keepAlive | false | 如果**true ,在代理连接上启用TCP KeepAlive以防止连接在TCP级别超时。这不应该与使用的KeepAliveInfo 消息混淆InactivityMonitor 。** |
logWriterName | default | 设置**org.apache.activemq.transport.LogWriter 要使用的实现的名称。名称映射到resources/META-INF/services/org/apache/activemq/transport/logwriters **目录中的类。 |
maximumConnections | Integer.MAX_VALUE | 此代理允许的最大套接字数。 |
minmumWireFormatVersion | 0 | **wireFormat 将被接受的最小远程版本(请注意拼写错误)。注意:当远程wireFormat 版本低于配置的最低可接受版本时,将引发异常并且将拒绝连接尝试。值0 表示不检查远程wireFormat **版本。 |
socketBufferSize | 64 * 1024 | 设置接受的套接字读写缓冲区的大小(以字节为单位)。 |
soLinger | Integer.MIN_VALUE | soLinger 值为时设置套接字的选项> -1 。当设置**-1 的soLinger **套接字选项被禁用。 |
soTimeout | 0 | 设置套接字的读取超时(以毫秒为单位)。值为**0 **表示没有超时。 |
soWriteTimeout | 0 | 设置套接字的写入超时(以毫秒为单位)。如果套接字写操作未在指定的超时之前完成,则套接字将被关闭。值0表示没有超时。 |
stackSize | 0 | 设置传输的后台读取线程的堆栈大小。必须以倍数指定**128K 。值为0 **表示忽略此参数。 |
startLogging | true | 如果传输堆栈**true 的TransportLogger 对象最初将消息写入日志。除非,否则忽略此参数trace=true **。 |
tcpNoDelay | false | 如果设置**true **了套接字选项 TCP_NODELAY 。这会禁用Nagle的小数据包传输算法。 |
threadName | N / A | 指定此参数时,将在调用传输期间修改线程的名称。附加远程地址,以便粘贴在传输方法中的调用将在线程名称中包含目标信息。当使用线程转储进行脱气时,这非常有用。 |
trace | false | 导致通过传输发送的所有命令都被记录。要查看记录的输出,请定义**Log4j 记录器:log4j.logger.org.apache.activemq.transport.TransportLogger=DEBUG **。 |
trafficClass | 0 | 要在套接字上设置的流量类。 |
diffServ | 0 | (仅限客户端)要在传出数据包上设置的首选差分服务流量类,如RFC 2475中所述。有效整数值:[0,64] 。有效的字符串值:EF ,AF[1-3][1-4] 或CS[0-7] 。使用JDK 6时,仅在JVM使用IPv4堆栈时才有效。要使用IPv4堆栈,请设置系统属性**java.net.preferIPv4Stack=true 。注意:同时指定’ diffServ和typeOfService** ’ 是无效的,因为它们在TCP / IP包头中共享相同的位置 |
typeOfService | 0 | (仅限客户端)要在传出数据包上设置的首选服务类型值。有效的整数值:[0,256] 。使用JDK 6时,仅在JVM配置为使用IPv4堆栈时才有效。要使用IPv4堆栈,请设置系统属性**java.net.preferIPv4Stack=true 。注意:同时指定’ diffServ和typeOfService** ’ 是无效的,因为它们在TCP / IP包头中共享相同的位置。 |
useInactivityMonitor | true | 当**false 该InactivityMonitor **被禁用,连接永不超时。 |
useKeepAlive | true | 在**true KeepAliveInfo 空闲连接上发送消息时,防止其超时。如果此参数是false **连接,如果在指定的时间内没有在连接上收到任何数据,则连接仍将超时。 |
useLocalHost | false | 当**true 本地连接将使用值进行localhost 的,而不是实际的本地主机名。在某些操作系统上,例如OS X ,无法以本地主机名连接,因此localhost **更好。 |
useQueueForAccept | true | 当**true **接受的套接字被放置到队列上以使用单独的线程进行异步处理时。 |
wireFormat | default | **wireFormat **要使用的工厂的名称。 |
wireFormat。* | N / A | 具有此前缀的属性用于配置**wireFormat **。 |
OpenWire Wire Format配置项:
OpenWire是ActiveMQ使用的默认有线格式。它为高速消息传递提供了高效的二进制格式。可以在JMS客户端的连接URI或代理的传输绑定URI上配置OpenWire选项。
注意,与传输协议的配置方式不同,无论是在在服务器端配置还是在客户端配置,都必须使用 wireFormat. *作为前缀,否则配置不生效。
选项 | 默认 | 描述 |
---|---|---|
cacheEnabled | true | 是否应该缓存通常重复的值,以便减少编组? |
cacheSize | 1024 | 当 cacheEnabled=true ,则此参数用于指定值的数量被缓存。 |
maxInactivityDuration | 30000 | 最大不活动持续时间(在套接字被认为死亡之前),以毫秒为单位。在某些平台上,套接字可能需要很长时间才能消亡。因此,允许代理在配置的时间段内处于非活动状态时终止连接。某些传输使用它来启用保持心跳功能。设置为值时禁用不活动监视<= 0 。 |
maxInactivityDurationInitalDelay | 10000 | 开始不活动检查之前的初始延迟。是的,这个词 'Inital' 应该像那样拼写错误。 |
maxFrameSize | MAX_LONG | 允许的最大帧大小。可以帮助防止OOM DOS攻击。 |
sizePrefixDisabled | false | 在每个数据包被封送之前,是否应该为数据包的大小添加前缀? |
stackTraceEnabled | true | 是否应将代理上发生的异常堆栈跟踪发送到客户端? |
tcpNoDelayEnabled | true | 不影响有线格式,但提供TCP_NODELAY 应在通信套接字上启用的对等体的提示 。 |
tightEncodingEnabled | true | 电线尺寸是否应优于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&k2=v2&k3=v3
传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="nio"
uri="nio://localhost:61617?maximumConnections=1000
&wireFormat.maxFrameSize=104857600"
&transport.threadName
&transport.trace=false
&transport.soTimeout=60000
&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.corePoolSize | 10 | 即使它们处于空闲状态,也要保留在池中的线程数 |
org.apache.activemq.transport.nio.SelectorManager.maximumPoolSize | 1024 | 池中允许的最大线程数 |
org.apache.activemq.transport.nio.SelectorManager.workQueueCapacity | 0 | 在增长池之前的最大工作队列深度 |
org.apache.activemq.transport.nio.SelectorManager.rejectWork | false | 当达到容量时,允许使用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&k2=v2&k3=v3
传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="amqp+nio"
uri="amqp+nio://localhost:61617?maximumConnections=1000
&wireFormat.maxFrameSize=104857600"
&transport.threadName
&transport.trace=false
&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&k2=v2&k3=v3
传输协议配置项前和有线配置前都要加前缀-->
<transportConnector name="auto+nio"
uri="auto+nio://localhost:61618?maximumConnections=1000
&wireFormat.maxFrameSize=104857600"
&transport.threadName
&transport.trace=false
&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&useUnicode=true&characterEncoding=UTF-8&userSSL=false&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 name | type | description |
---|---|---|
AMQ_SCHEDULED_DELAY | long | The time in milliseconds that a message will wait before being scheduled to be delivered by the broker |
AMQ_SCHEDULED_PERIOD | long | The time in milliseconds to wait after the start time to wait before scheduling the message again |
AMQ_SCHEDULED_REPEAT | int | The number of times to repeat scheduling a message for delivery |
AMQ_SCHEDULED_CRON | String | Use 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)中,不再重复发送。
关于消息重试机制,可以设置的属性有以下几个:
Property | Default Value | Description |
---|---|---|
initialRedeliveryDelay | 1000L | 首次重发等待时间 |
redeliveryDelay | 1000L | 重发等待时间,默认为1s,initialRedeliveryDelay=0时才有效。一般使用initialRedeliveryDelay,两者等价 |
useExponentialBackOff | false | 是否指数性的增长重发等待时间 |
backOffMultiplier | 5 | 重发等待时间每次增长的倍数 |
maximumRedeliveries | 6 | 最大的重发次数 |
maximumRedeliveryDelay | -1 | 最大的重发等待时间。-1表示无限制 |
useCollisionAvoidance | false | Should the redelivery policy use collision avoidance. |
collisionAvoidanceFactor | 0.15 | The 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的查询效率比较高。