ActiveMQ传输协议
概述
activemq传输协议的官方文档:http://activemq.apache.org/configuring-version-5-transports.html
ActiveMQ支持的client-broker通讯协议有:TVP、NIO、UDP、SSL、Http(s)、VM。
协议的配置文件为:%activeMQ安装目录%/conf/activemq.xml
<transportConnectors>
<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:1884?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
协议不同、代码不同
协议 | 描述 |
---|---|
TCP | 默认的协议,性能相对一般 |
NIO | 基于TCP协议之上进行了扩展和优化具有更好的扩展性 |
UDP | 性能比TCP好,但不具备可靠性 |
SSL | 安全连接 |
HTTP(S) | 基于HTTP与HTTPS |
VM | 客户端和代理在同一台虚拟机时直接通信 |
TCP协议
- Transmission Control Protocol(TCP)是默认的,TCP的Client监听端口61616;
- 在网络传输数据前,必须要先序列化数据,消息是通过一个叫wire protocol的来序列化成字节流;
- TCP连接的URI形式如:
tcp://HostName:port?key=value&key=value
,后面的参数是可选的。
NIO协议
NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的Client调用和服务器端有更多的负载。
NIO连接的URI形式:nio://hostname:port?key=value&key=value
在ActiveMQ中添加NIO协议:
- 修改配置文件activemq.xml在
<transportConnectors>
节点下添加如下内容:
<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />
- 修改完成后重启ActiveMQ:
service activemq restart
- 查看管理后台,可以看到页面多了nio
- 客户端连接时把url改为:
private static final String ACTIVEMQ_URL = "nio://192.168.25.131:61618";
NIO协议增强
上面的配置是Openwire协议传输底层使用NIO网络IO模型。还可以让其他协议传输(amqp、mqtt等)底层也使用NIO网络IO模型,只需在activemq.xml中加入:
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61608?maximumConnections=1000&wireFormat.maxFrameSize=104857600&org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20&org.apache.activemq.transport.nio.Se1ectorManager.maximumPoo1Size=50"/>
- auto:针对所有的协议,会自动识别调用的是什么协议。
- nio:使用NIO网络IO模型。
修改配置文件后重启ActiveMQ,这样所有的协议底层都使用NIO。
ActiveMQ消息存储与持久化
可参考:https://www.cnblogs.com/Soprano/p/10659576.html
持久化机制是ActiveMQ高可用的一种保障机制,使得ActiveMQ服务器宕机了,消息不会丢失的机制。
ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。就是在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等。再试图将消息发给接收者,成功则将消息从存储中删除,失败则继续尝试尝试发送。
消息中心启动以后,要先检查指定的存储位置是否有未成功发送的消息,如果有,则会先把存储位置中的消息发出去。
AMQ Message Store
基于文件的存储机制,是以前的默认机制,现在不再使用。
AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特点。消息存储将一个个文件中文件的默认大小为32M,当一个文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。AMQ适用于ActiveMQ5.3之前的版本。
KahaDB
KahaDB是从ActiveMQ 5.4开始默认的持久化插件。KahaDb恢复时间远远小于其前身AMQ并且使用更少的数据文件,所以可以完全代替AMQ,kahaDB的持久化机制同样是基于日志文件,索引和缓存。
配置文件activemq.xml中,如下:
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
日志文件的存储目录在:%activemq安装目录%/data/kahadb
内部结构
消息存储在基于文件的数据日志中。如果消息发送成功,变标记为可删除的。系统会周期性的清除或者归档日志文件。消息文件的位置索引存储在内存中,这样能快速定位到。定期将内存中的消息索引保存到metadata store中,避免大量消息未发送时,消息索引占用过多内存空间。
- Data logs:消息日志包含了消息日志和一些命令
- Cache:当有活动消费者时,用于临时存储,消息会被发送给消费着,同时被安排将被存储,如果消息及时被确认,这不需要写入到磁盘
- Btree indexes(消息索引):用于引用消息日志(message id),它存储在内存中,这样能快速定位到。MQ会定期将内存中的消息索引保存到 metadata store 中,避免大量消息未发送时,消息索引占用过多内存空间。
- Redo log:用于在非正常关机情况下维护索引完整性。
目录结构(4 + 1)
消息存储使用一个事务日志和仅仅用一个索引文件来存储其所有地址
- Db log files:用于存储消息(默认大小32M),当log日志满了,会创建一个新的,当 log 日志中的消息都被删除,该日志文件会被删除或者归档(存到Archive directory)。
- Archive directory:当 datalog不在被kahadb需要会被归档(通过 archiveDataLogs 属性控制)。
- Db.data:存放 Btree indexs(B树索引)。
- Db.redo:存放 redo file,用于恢复 Btree indexs。
- lock:文件读写锁
JDBC存储
支持通过JDBC将消息存储到关系数据库,由于依赖其它数据库,性能上自然不如文件存储,但能通过关系型数据库查询到消息的信息。
MQ 支持的数据库:Apache Derby、MySQL、PostgreSQL、Oracle、SQLServer、Sybase、Informix等。
默认生成三张表:
表1:ACTIVEMQ_MSGS:用于存储消息,Queue和Topic都存储在这个表中:
字段 | 说明 |
---|---|
ID | 自增的数据库主键 |
CONTAINER | 消息的Destination |
MSGID_PROD | 消息发送者客户端的主键 |
MSG_SEQ | 是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID |
EXPIRATION | 消息的过期时间,存储的是从1970-01-01到现在的毫秒数 |
MSG | 消息本体的Java序列化对象的二进制数据 |
PRIORITY | 优先级,从0-9,数值越大优先级越高 |
使用queue模式持久化,发布3条消息后,发现ACTIVEMQ_MSGS数据表多了N条数据:
启动消费者,消费了所有的消息后,数据表的数据就消失了。
表 2:ACTIVEMQ_ACKS:用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存:
字段 | 说明 |
---|---|
CONTAINER | 消息的Destination |
SUB_DEST | 如果是使用Static集群,这个字段会有集群其他系统的信息 |
CLIENT_ID | 每个订阅者都必须有一个唯一的客户端ID用以区分 |
SUB_NAME | 订阅者名称 |
SELECTOR | 选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性AND和OR操作 |
LAST_ACKED_ID | 记录消费过的消息的ID |
注册一个消费者时,ACTIVEMQ_ACKS数据表就多了一个消费者的身份信息。一条记录代表一个持久化topic消费者。
注:我们启动持久化生产者发布3个数据,ACTIVEMQ_MSGS数据表新增3条数据,消费者消费所有的数据后,ACTIVEMQ_MSGS数据表的数据并没有消失。持久化topic的消息不管是否被消费,是否有消费者,产生的数据永远都存在,且只存储一条。这个是要注意的,持久化的topic大量数据后可能导致性能下降。
表 3:ACTIVEMQ_LOCK(消息锁,保证同一时间只能有一个broker访问这些表结构)
表 activemq_lock在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker。
JDBC Message Store with ActiveMQ Journal
这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库读库。ActiveMQ Journal使用高速缓存写入技术,大大提高了性能。当消费者的速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。
为了高性能,这种方式使用日志文件存储+数据库存储。先将消息持久到日志文件,等待一段时间再将未消费的消息持久到数据库。该方式要比JDBC性能要高。
LevelDB
新兴的技术
这种文件系统是从ActiveMQ5.8以后引进,也是基于文件的本地数据库存储形式,但是比 KahaDB更快,它比KahaDB 更快的原因是它不使用BTree 索引,而是使用本身自带的 LeavelDB 索引。
LevelDB是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库,由Google发起并开源。LevelDB中的核心设计算法是跳跃表(Skip List),核心操作策略是对磁盘上的数据日志结构进行归并(LSM)。
总结
说明 | |
---|---|
AMQ | 基于日志文件 |
KahaDB | 基于日志文件,从ActiveMQ5.4开始默认使用 |
JDBC | 基于第三方数据库 |
Replicated LevelDB Store | 从5.9开始提供了LevelDB和Zookeeper的数据复制方法,用于Master-slave方式的首选数据复制方案。 |
jdbc效率低,kahaDB效率高,jdbc+Journal效率较高。
ActiveMQ集群
参考:https://blog.youkuaiyun.com/yinwenjie/article/details/51205822
基于共享文件系统的集群
基于共享文件系统的热备方案可以说是ActiveMQ消息中间件中最早出现的一种热备方案。它的工作原理很简单:让若干个ActiveMQ服务节点,共享一个文件系统。当某一个ActiveMQ服务抢占到了这个文件系统的操作权限,就给文件系统的操作区域加锁;其它服务节点一旦发现这个文件系统已经被加锁(并且锁不属于本进程),就会自动进入Salve模式。
当某个ActiveMQ节点获取了文件系统的操作权限后,首先做的事情就是从文件系统中恢复内存索引结构:KahaDB恢复BTree结构;LevelDB恢复memTable结构。
基于共享关系型数据库的集群
当需要搭建热备方案时,两个或者更多的ActiveMQ服务节点共享同一个数据服务。首先抢占到数据库服务的ActiveMQ节点,会将数据库中“activemq_lock”数据表的Master状态标记为自己,这样其它ActiveMQ服务节点就会进入Salve状态。
Zookeeper+LevelDB搭建集群
从ActiveMQ V5.9.0+ 版本开始基于Zookeeper+LevelDB搭建集群,集群仅提供主备方式的高可用集群功能,避免单点故障。这个方案中,我们可以让每个节点都有自己独立的LevelDB数据库(不是上面那样共享LevelDB的工作目录),并且使用Zookeeper集群控制多个ActiveMQ节点的工作状态,完成Master/Salve状态的切换。
使用Zookeeper集群注册所有的ActiveMQ Broker,但只有其中的一个Broker可以提供服务,它将被视为Master,其他的Broker处于待机状态被视为Slave。
如果Master因故障而不能提供服务,Zookeeper从Slave中选出一个Broker充当Master。Slave连接Master并同步他们的存储状态,Slave不接受客户端连接。所有的存储操作都将复制到连接至Master的Slaves。
集群搭建参考:https://blog.51cto.com/4925054/2103719
高级特性
异步投递
ActiveMQ支持同步和异步发送消息到broker,默认异步。当设定同步发送的方式或未使用事务的情况下发持久化消息,这时是同步的。
如果没有使用事务,且发送的是持久化消息,每次发送都会阻塞一个生产者直到broker发回一个确认,这样做保证了消息的安全送达,但是会阻塞客户端,造成很大延时 。
在高性能要求下,可以使用异步提高producer的性能。但会消耗较多的client端内存,也不能完全保证消息发送成功。在useAsyncSend = true
情况下容忍消息丢失。
异步投递确认发送成功:
异步发送消息丢失的情况场景是:UseAsyncSend为true使用producer.send持续发送消息,消息不会阻塞,生产者会认为所有的send消息均会被发送到MQ,如果MQ突然宕机,此时生产者端尚未同步到MQ的消息均会丢失 。 故正确的异步发送方法需要接收回调。
public class Jms_TX_Producer {
private static final String ACTIVEMQ_URL = "tcp://118.24.20.3:61626";
private static final String ACTIVEMQ_QUEUE_NAME = "Async";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(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)session.createProducer(queue);
try {
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("tx msg--" + i);
textMessage.setJMSMessageID(UUID.randomUUID().toString()+"orderAtguigu");
final String msgId = textMessage.getJMSMessageID();
activeMQMessageProducer.send(textMessage, new AsyncCallback() {
public void onSuccess() {
System.out.println("成功发送消息Id:"+msgId);
}
public void onException(JMSException e) {
System.out.println("失败发送消息Id:"+msgId);
}
});
}
System.out.println("消息发送完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
activeMQMessageProducer.close();
session.close();
connection.close();
}
}
}
同步发送和异步发送的区别就在于 :
- 同步发送send不阻塞就代表消息发送成功;
- 异步发送需要接收回执并又客户端在判断一次是否发送 ;
延时投递与定时投递
首先在配置文件中设置定时器开关为true
schedulerSupport="true"
可设置的参数为:
long delay = 3 * 1000 ;
long perid = 4 * 1000 ;
int repeat = 7 ;
for (int i = 1; i < 4 ; i++) {
TextMessage textMessage = session.createTextMessage("delay msg--" + i);
// 消息每过3秒投递,每4秒重复投递一次,一共重复投递7次
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,delay);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,perid);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,repeat);
messageProducer.send(textMessage);
}
消费重试机制
消费者收到消息,之后出现异常了,没有告诉broker确认收到该消息,broker会尝试再将该消息发送给消费者。尝试n次,如果消费者还是没有确认收到该消息,那么该消息将被放到死信队列重,之后broker不会再将该消息发送给消费者。
具体哪些情况会引发消息重发:
- Client用了transactions且再session中调用了rollback
- Client用了transactions且再调用commit之前关闭或者没有commit
- Client再CLIENT_ACKNOWLEDGE的传递模式下,session中调用了recover
消息重发时间间隔为1s和重发次数为6次
一个消息被redelivedred超过默认的最大重发次数(默认6次)时,消费者回复“poison ack”表示这个消息有毒,告诉broker不要再发了。这个时候broker会把这个消息放到DLQ(死信队列)。
避免重复消费
网络延迟传输中,会造成MQ进行重试,重试过程中可能会造成重复消费。
如果消息是做数据库的插入操作,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
还可以准备一个第三方服务来记录消费。以Redis为例,给消息分配一个全局id,只要消费过该消息,将<id, message>以K-V形式写入Redis,消费者在消费前,先去Redis中查找有没有消费记录即可。