activemq查漏补缺
基础
带日志自定义启动
./activemq start > /myactiveMQ/run_time.log
注意如果启动不了amq,有一种原因是:linux机器名带有下滑线_,腾讯云服务器也会带下划线,会在日志中报错。
activemq图解
通过ConnectionFactory获取connection,connection生成Session,然后session可以生成Message,MessageProducer和MessageConsumer。
消费者
一个生产者生产12条消息,两个消费者并列在线,会轮流
消息基础JMS
JMS消息头:
- @param: deliveryMode: whether re-delivery
send(Destination destination, int deliveryMode, int priority, long timeToLive)
-
@Expiration: 默认不过期, 如果timeToLive = 0, 永不过期
-
@Priority:0-9 十个级别,0-4是普通消息,5-9是加急消息
JMS不要求严格按照十个等级发消息,但必须保证加急消息要先于普通消息到达,默认是4级
- @JMSMessageID:唯一识别每个消息ID,可以使用分布式ID生成器生成
JMS消息体:
-
TextMessage:
-
MapMessage:key为String类型,值为Java的基本类型
-
BytesMessage:二进制数组消息,包含一个byte[]
-
StreamMessage:Java数据流消息,用标准流操作来顺序地填充和读取
-
ObjectMessage:对象消息,包含一个可序列化的Java对象
JMS消息属性:
如果需要除消息头字段以外的值,可以使用消息属性。识别/去重/重点标注等操作非常有用的方法。
消息可靠性
- 持久性:persistent:
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
非持久化消息重启broker后,消息会丢失!持久化消息重启broker后,消息仍然存在。
默认就是开启持久化。
持久化Topic:先启动订阅再启动生产
下述代码把connection.start放到后面保证其创建客户端ID和创建订阅者后,先向ActiveMQ注册订阅者,无论消费者是否在线都会接受到订阅消息,不在线的话,重新连接后会获取所有没收到过的消息。之后再接受订阅。(类似于先关注微信公众号,再接受推送)
connection = ActiveMQConnection.makeConnection("tcp://192.168.157.133:31616");
connection.setClientID("z3");
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark");
// 开启connection,表明先订阅,再接收消息
connection.start();
Message receive = topicSubscriber.receive(2000);
while (null != receive) {
extMessage textMessage = (TextMessage) receive;
System.out.println(textMessage.getText());
receive = topicSubscriber.receive(5000);
}
- 事务:transatction:
两个参数:一个叫做事务,一个叫做签收
事务偏生产者,设置为false,执行send方法进入消息队列。设置为true,执行send,还是0条消息进入消息队列,在session关闭前,提交commit。
生产者侧事务:
try {
// ok
session.commit();
} catch (Exception e) {
e.printStackTrace();
// error
session.rollback();
} finally {
if (null != session)
session.close();
}
消费者侧如果设置为true,但是没有commit,拿到消息仍然算没有消费,会一直重复消费。提交commit可以避免消息重复消费。
- 签收:acknowledge:
Session.DUPS_OK_ACKNOWLEDGE 允许多次签收
Session.AUTO_ACKNOWLEDGE 自动签收
CLIENT_ACKNOWLEDGE 客户端自动签收
SESSION_TRANSACTED 事务签收
如果session开启事务(transacted:true)并且开启客户端手动签收。再代码加上 commit后,会自己签收掉,不会重复消费。 如果开启事务,不开启commit,手动签收,仍然会重复消费。
结论:
- 事务 >>>> 签收
如果事务回滚,则消息会被再次传送。
非持久订阅,只有当消费者在线(和mq保持联系的时候)才能获取订阅消息。持久化订阅,消费者先向mq注册客户端ID,然后上线可以获取未收取的消息。
Java代码嵌入broker
activemq制定配置文件启动:
./activemq start xbean:file:/usr/local/amq/amq_01/conf/activemq.xml
嵌入broker:
- POM.xml
<!--java code embedded small activemq broker-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>
- EmbedBroker
public static void main(String[] args) throws Exception {
BrokerService brokerService = new BrokerService();
brokerService.setUseJmx(true);
brokerService.addConnector("tcp://localhost:61619");
brokerService.start();
}
写个测试发现嵌入broker正常工作。
- 队列验证
spring整合activemq
- pom.xml
重要的包(并不是所有)
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.13.2</version>
</dependency>
springboot整合activemq
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</ artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<version>1.5.0.RELEASE</version>
</dependency>
application.yml:
server:
port: 7777
spring:
activemq:
broker-url: tcp://192.168.157.133:31616
user: admin
password: admin
jms:
pub-sub-domain: false # false = Queue true = Topic
myqueue: boot-amq-queue
ConfigBean.java
package com.xxx.mq.springboot;
@Component
@EnableJms
public class ConfigBean {
@Value("${myqueue}")
private String myQueue;
@Bean
public Queue queue() {
return new ActiveMQQueue(myQueue);
}
}
Producer:
package com.xxx.mq.springboot;
@Component
public class QueueProducer {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Queue queue;
public void produceMsg() {
jmsMessagingTemplate.convertAndSend(queue, "****"+ UUID.randomUUID().toString().substring(0, 6));
}
}
主启动类:
package com.xxx.mq.springboot;
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
Springboot单元测试类:
import javax.annotation.Resource;
@SpringBootTest(classes = MainApp.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration // 微服务 以web形式
public class TestAMQ {
@Resource
private QueueProducer queueProducer;
@Test
public void testSend() throws Exception {
queueProducer.produceMsg();
}
}
可以直接使用springboot的 @JmsListener 来监听
activemq 传输协议
URI描述信息的头部都是采用协议名称:
描述amqp协议的监听端口,采用URI描述格式 “amqp://…”
唯独用openwire协议使用: “tcp://…”,amq默认协议openwire,消息是通过 wire protocol来序列化成字节流。默认amq把 wire protocol 叫做OpenWire,促使消息数据快速交互。
TCP
TCP协议优点:
-
TCP协议传输可靠,稳定性强
-
字节流方式传递,效率高
-
应用广泛,支持任何平台
NIO
NIO 基础
基于TCP协议之上,使用NIO会有更好的性能.普通协议使用BIO模型,效率比较低。
修改activemq配置文件:
<transportConnectors>
<transportConnector name="nio" uri="nio://0.0.0.0:31618?trace=true"/>
</transportConnectors>
启动后发现activemq控制台 network 会出现 nio
把 JAVA 代码中的 URL 链接改一下,就可以使用 NIO。
NIO 增强
让某个端口支持 NIO 网络IO模型,又支持多个协议:
5.13.0后, amq能自动检测协议
-
使用auto关键字
-
使用 “+” 为端口设置多种特性
<transportConnectors>
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:31608?maximumConnections=1000&wireFormat.maxFrameSize=104857600&org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20&org.apache.activemq.transport.nio.SelectorManager.maximumPoolSize=50"/>
</transportConnectors>
可以使用tcp和nio开头的url,自动适配协议。使用mqtt等可能会报错,因为不同协议的java代码可能不同,mqtt 需要 mqttClient 。
TCP + BIO 默认
NIO + TCP
NIO + TCP/Mqtt/
其他协议
SSL 安全链接
STOMP Streaming Text Orientated Message Protocol, 流文本定向消息协议。
<transportConnector name="stomp" uri="stomp://0.0.0.0:31613?maximumCConnections=1000&wireFormat.maxFrameSize=104857600"/>
VM VM本身不是协议,当客户端和代理在同一个JVM中,通信可以不通过网络通道,而是直接通信,使用VM
AUTO
AMQP Advanced Message Queuing Protocol,高级消息队列协议
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:3883?maximumConnnections=1000&wireFormat.maxFrameSize=104857600"/>
WS WebsSocket
ActiveMQ 消息存储和持久化
JDBC
安装 MQ 和 MySQL
把mysql数据库驱动包放到amq lib文件夹下
wget https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.38/mysql-connector-java-5.1.38.jar
jdbcPersistenceAdapter 配置:
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup = "true"/>
</persistenceAdapter>
createTablesOnStartup : 是否在启动的时候创建数据表。首次true,重启后改成false。
创建数据库 bean:
dbcp是默认带的jar包,使用c3p0等别的需要额外倒入jar包
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.157.133:3306/demo?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="maxTotal" value="200"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
mysql创建对应数据库
然后 activemq 会自动创建以下三张表:
ACTIVEMQ_MSGS, ACTIVEMQ_ACKS, ACTIVEMQ_LOCK
ACTIVEMQ_MSGS:记录消息(签收删除)
字段名 | 意义 |
---|---|
ID | 自增数据库主键 |
CONTAINER | 消息的Destination |
MSGID_PROD | 消息发送者的主键 |
MSG_SEQ | 消息发送到顺序, MSGID_PROD + MSG_SEQ 可组成JMS的MessageID |
EXPIRATION | 消息的过期时间,存储的是 1970-01-01到现在的毫秒数 |
MSG | 消息本体的Java序列化对象的二进制数据 |
PRIORITY | 优先级 |
ACTIVEMQ_ACKS: 记录订阅者(删除删除)
字段名 | 意义 |
---|---|
CONTAINER | 消息的Destination |
SUB_DEST | 如果使用Static集群,这个字段会有集群其他系统的信息 |
CLIENT_ID | 每个订阅者都有一个唯一的客户端ID用以区分 |
SUB_NAME | 订阅者名字 |
SELECTOR | 消息选择器 |
LAST_ACKED_ID | 记录消费过的消息的ID |
ACTIVEMQ_LOCK:
在集群环境中才有用,只有一个Broker可以获取消息,称为Mater Broker
下划线坑爹:操作系统名字有下划线,无法启动 activemq
jdbc 和高性能日志
journal文件先记录amq消息,批量定期写入DB,提升性能。
<persistenceFactory>
<journalPersistenceAdapterFactory
journalLogFileSize="32768"
useJournal="true"
useQuickJournal="true"
dataSource="#mysql-ds"
dataDirectory="activemq-data"/>
</persistenceFactory>
数据不一定会立刻出现在DB中,会有延迟。如果生产1000条消息,消费了900条,这样只需要写入100条到数据库就行。
LevelDB
和KahaDB类似,也是基于文件的本地数据库储存形式,但是比KahaDB更快。它不使用自定义的BTree索引,而是用基于LevelDB的索引。
默认配置:
<persistenceAdapter>
<levelDBdirectory = "activemq-data"/>
</persistenceAdapter>
KahaDB
基于日志文件,基于文件的持久化数据库,从5.4之后默认持久化插件。类似于 redis aof
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
kahadb 路径下有以下文件:
db-1.log db.data db.redo lock db.free
KahaDB 消息存储使用一个事物日志和仅仅用一个索引文件来存储它所有的地址,数据被追加到data logs中,当log文件中的数据不再需要的时候,log文件会被丢弃。
-
db-1.log 文件大小到限制时,会自动创建db-2.log文件。文件会被归档或者删除,当数据文件不再被引用后
-
db.data 包含了持久化的BTree索引,BTree作为索引指向db-1.log中的数据
-
db.free 当前db.data文件里哪些页面是空闲的,文件具体内容是所有空闲页ID
-
db.redo 用来进行消息恢复,如果KahaDB在强制退出后启动,用于恢复BTree索引
-
lock文件锁,表示获取当前读写权限的broker
AMQ
AMQ Message Store。基于文件的储存,现在已经弃用
Replicated levelDB
基于ZooKeeper和levelDB搭建ActiveMQ集群,提供主从集群
-
拷贝三份 activemq 文件夹
-
修改 三个 activemq 的默认 61616 和 8161 端口,一个在 activemq.xml 一个在 jetty.xml 文件中
-
host 名字映射
-
修改三个broker name,使得他们名字都一样
-
三个节点持久化配置一样
replicas 指的是 复制的机器数
bind 地址需要不同机器 不通地址
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63631"
zkAddress="localhost:2191, xxx, xxx"
hostname="xxx-server"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
-
修改 三个配置文件绑定的 61616端口,三个不同 (如果在一个机器上)
-
启动 三台 zk 启动 三台 mq
-
查看集群状态
activemq 面试题重点知识
异步投递
对于一个慢点消费者,可能会阻塞producer,所以使用异步。异步投递可以显著的提高发送性能。AMQ默认使用异步,除非指定使用同步方式或者在未使用事务的前提下发送持久化的消息。
当使用非事务并且发送的是持久化消息(不能保证一致性),每一次发送消息都会同步发送且阻塞producer直到broker返回一个确认,表示消息已经被安全的持久化到磁盘。保障了消息安全,但是阻塞了客户端。
缺点:
消耗较多Client端内存,和导致broker性能消耗增加
不能保证消息发送成功,在useAsyncSend=true的情况下客户端要容忍消息丢失的可能。
开启异步(默认开启),三种方式:
ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://localhost:61616?jms.useAsyncSend=true");
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory();
cf.setUseAsyncSend(true);
connection = ActiveMQConnection.makeConnection(AMQ_URL);
connection.setUseAsyncSend(true);
异步如何保证消息发送成功?
如果MQ突然宕机,生产者内存中尚未被发送至MQ的消息都会丢失。
所以正确的异步发需要回调确认:
下述的 new AsyncCallback() 就是回调函数
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(destination);
activeMQMessageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
connection.start();
TextMessage message = session.createTextMessage("myTextMessage");
message.setJMSMessageID(UUID.randomUUID().toString().substring(0, 6));
String msgID = message.getJMSMessageID();
activeMQMessageProducer.send(message, new AsyncCallback() {
@Override
public void onSuccess() {
System.out.println(msgID + "has been sent");
}
@Override
public void onException(JMSException e) {
System.out.println(msgID + "has failed");
}
});
延时投递 和 定时投递
需要在xml配置文件中,broker标签打开 schedulerSupport
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}" schedulerSupport="true">
参数:
AMQ_SCHEDULED_DELAY long 延时投递的时间
AMQ_SCHEDULED_PERIOD long 重复投递的间隔
AMQ_SCHEDULED_REPEAT int 重复投递次数
AMQ_SCHEDULED_CRON String Cron表达式
Java 代码里面封装的辅助消息类型: ScheduledMessage
TextMessage message = session.createTextMessage("DelayMyTextMessage " + UUID.randomUUID().toString().substring(0, 6));
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, 10000);
message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
activeMQMessageProducer.send(message);
AMQ 消费重试机制
哪些情况会引起消息重发:
-
Client用了transations且在session中调用了rollback()
-
Client用了transations且在调用commit()之前关闭或没有commit
-
Client在CLIENT_ACKNOWLEDGE的传递模式下,在session中调用recover()
有毒消息Poison ACK:
一个消息被重发超过默认次数(6),消费端会给MQ发送一个poison ack,broker会把它放入DLQ (死信队列)
初始化重发时间间隔: 1s
initialRedeliveryDelay = 1000L
maximumRedeliveries = 6
可以通过 RedeliveryPolicy 来设置重发次数:
ActiveMQConnectionFactory acf = new ActiveMQConnectionFactory(AMQ_URL);
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
redeliveryPolicy.setMaximumRedeliveries(3);
acf.setRedeliveryPolicy(redeliveryPolicy);
connection = acf.createConnection();
connection.start();
死信队列 Dead Letter Queue
一般生产环境中设计两个队列:业务队列,死信队列
默认把所有的DeadLetter保存在一个共享的队列中,这是AMQ默认策略。 ActiveMQ.DLQ,也可以用以下配置来设置额
<deadLetterStrategy>
<sharedDeadLetterStrategy deadLetterqueue="DLQ-QUEUE"/>
</deadLetterStrategy
默认,无论topic还是queue,broker都会使用Queue来存放DeadLetter,也可以指定使用Topic:(还可以设置队列前缀)
<policyEntry queue="order">
<deadLetterStrategy>
<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="false"/>
</deadLetterStrategy>
</policyEntry>
自动删除过期消息:
processExpired = true, 默认表示放入死信队列
processNonExpired = false,默认不会把非持久的死消息发到死信队列中
<policyEntry queue=">" >
<deadLetterStrategy>
<shareDeadLetterStrategy processExpired="false" processNonPersistent="true" />
</deadLetterStrategy>
</policyEntry>
避免重复消费 与 幂等性
如果消息是做数据库的插入操作,给这个消息做一个唯一主键,不会出现重复消费。
如果还不行,可以使用第三方服务,例如redis,给消息分配一个全局id,如果消费过,就把记录放到redis中,其他消费者消费前只要查询redis即可。