消息队列学习-ActiveMQ(六)
11 ActiveMQ的多节点集群
11.1 面试题
引入消息中间件后如何保证其高可用?
11.2 是什么
基于zookeeper和LevelDB搭建ActiveMQ集群。集群仅提供主备方式的高可用集群功能,避免单点故障。
11.3 zookeeper+replicated-leveldb-store的主从集群
11.3.1 三种集群方式对比
- 基于shareFileSystem共享文件系统(KahaDB)
- 基于JDBC
- 基于可复制的LevelDB
11.3.2 本次案例采用ZK+Replicated LevelDB Store
11.3.2.1 是什么
从ActiveMQ5.9开始,ActiveMQ的集群实现方式取消了传统的Masster-Slave方式.
增加了基于Zookeeper+LevelDB的Master-Slave实现方式,从5.9版本后也是官网的推荐
基于Zookeeper和LevelDB搭建ActiveMQ集群,集群仅提供主备方式的高可用集群功能,避免单点故障.
11.3.2.2 官网集群原理图
使用Zookeeper集群注册所有的ActiveMQ Broker但只有其中一个Broker可以提供服务,它将被视为Master,其他的Broker处于待机状态被视为Slave。
如果Master因故障而不能提供服务,Zookeeper会从Slave中选举出一个Broker充当Master。Slave连接Master并同步他们的存储状态,Slave不接受客户端连接。所有的存储操作都将被复制到连接至Maste的Slaves。
如果Master宕机得到了最新更新的Slave会变成Master。故障节点在恢复后会重新加入到集群中并连接Master进入Slave模式。
所有需要同步的消息操作都将等待存储状态被复制到其他法定节点的操作完成才能完成。所以,如给你配置了replicas=3,name法定大小是(3/2)+1 = 2。Master将会存储更新然后等待(2-1)=1个Slave存储和更新完成,才汇报success。
有一个node要作为观察者存在。当一个新的Master被选中,你需要至少保障一个法定mode在线以能够找到拥有最新状态的ode,这个ode才可以成为新的Master。因此,推荐运行至少3个replica nodes以防止一个node失败后服务中断。
11.4 部署规划和步骤
11.4.1 配置zookeeper分布式环境
详细配置情况:https://blog.csdn.net/qq_39410381/article/details/106167471,这里不再赘述。
启动zookeeper
# 三台都执行一下
/export/services/zookeeper-3.4.9/bin/zkServer.sh start
# 查看zookeeper状态
/export/services/zookeeper-3.4.9/bin/zkServer.sh status
这里截node1图举例
# 关闭服务
/export/services/zookeeper-3.4.9/bin/zkServer.sh status
# 启动zookeeper的客户端
/export/services/zookeeper-3.4.9/bin/zkCli.sh -server localhost:2181
11.4.2 集群部署规划列表
主机 | Zookeeper集群端口 | AMQ集群bind端口 | AMQ消息tcp端口 | 管理控制台端口 | AMQ节点安装目录 |
---|---|---|---|---|---|
192.168.188.100 | 2181 | bind=“tcp://0.0.0.0:63631” | 61616 | 8161 | /myactiveMQ/apache-activemq-5.15.12 |
192.168.188.110 | 2181 | bind=“tcp://0.0.0.0:63632” | 61616 | 8162 | /myactiveMQ/apache-activemq-5.15.12 |
192.168.188.120 | 2181 | bind=“tcp://0.0.0.0:63633” | 61616 | 8163 | /myactiveMQ/apache-activemq-5.15.12 |
11.4.3 创建3台集群目录
位置如下图所示:
11.4.4 修改管理控制台端口
cd /myactiveMQ/apache-activemq-5.15.12/conf/
vim jetty.xml
node01配置:
node02配置:
node03配置:
11.4.5 hostname名字映射
vim /etc/hosts
11.4.6 ActiveMQ集群配置
(1)配置文件里面的BrokerName要全部一致
vim /myactiveMQ/apache-activemq-5.15.12/conf/activemq.xml
(2)持久化配置
vim /myactiveMQ/apache-activemq-5.15.12/conf/activemq.xml
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63631"
zkAddress="node01:2181,node02:2182,node03:2183"
hostname="node01"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
node01配置:
node02配置:
node03配置:
11.4.7 修改各个节点的消息端口
伪分布式才配置,这里不用
11.4.8 按顺序启动3个ActiveMQ节点
到这步前提是zk集群已经成功启动运行
cd /myactiveMQ/apache-activemq-5.15.12/bin/
./activemq start
11.4.9 zk集群节点状态说明
cd /export/services/zookeeper-3.4.9/bin/
./zkCli.sh -server node01:2181
ls /
ls /activemq
ls /activemq/leveldb-stores
get /activemq/leveldb-stores/00000000000
get /activemq/leveldb-stores/00000000001
get /activemq/leveldb-stores/00000000002
"elected"非null为master
11.5 集群可用性测试
首先修改代码,只需在生产者和消费者源代码基础上改两行:
public static final String ActiveMQ_URL = "failover:(tcp://node01" +
":61616,tcp://node02:61616,tcp://node03:61616)?" +
"randomize=false";
public static final String QUEUE_NAME = "queue-cluster";
(1)正常测试
连接到的是61616,成功。
(2)宕机测试
kill -9 pid
再次运行生产者,其他两台中任意一台
启动成功,即为成功。
12 高级特性和大厂常考重点
12.1 引入消息队列之后该如何保证其高可用性
zookeeper+Replicated LevelDB
12.2 异步投递Async Sends
12.2.1 异步投递
对于一个Slow Consumer,使用同步发送消息可能出现Producer堵塞的情况,慢消费者适合使用异步发送
12.2.2 是什么
ActiveMQ支持同步,异步两种发送的模式将消息发送到broker,模式的选择对发送延时有巨大的影响。producer能达到怎么样的产出率(产出率=发送数据总量/时间)主要受发送延时的影响,使用异步发送可以显著提高发送的性能。
ActiveMQ默认使用异步发送的模式:除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送的。
如果你没有使用事务且发送的是持久化的消息,每一次发送都是同步发送的且会阻塞producer知道broker返回一个确认,表示消息已经被安全的持久化到磁盘。确认机制提供了消息安全的保障,但同时会阻塞客户端带来了很大的延时。
很多高性能的应用,允许在失败的情况下有少量的数据丢失。如果你的应用满足这个特点,你可以使用异步发送来提高生产率,即使发送的是持久化的消息。
异步发送
它可以最大化producer端的发送效率。我们通常在发送消息量比较密集的情况下使用异步发送,它可以很大的提升Producer性能;不过这也带来了额外的问题,
就是需要消耗更多的Client端内存同时也会导致broker端性能消耗增加;
此外它不能有效的确保消息的发送成功。在userAsyncSend=true的情况下客户端需要容忍消息丢失的可能。
12.2.3 官网配置
12.2.4 面试追问:异步消息如何确定发送成功?
异步发送丢失消息的场景是:生产者设置UserAsyncSend=true
,使用producer.send(msg)持续发送消息。
由于消息不阻塞,生产者会认为所有send的消息均被成功发送至MQ。
如果MQ突然宕机,此时生产者端内存中尚未被发送至MQ的消息都会丢失。
所以,正确的异步发送方法是需要接收回调的。
同步发送和异步发送的区别就在此:
- 同步发送等send不阻塞了就表示一定发送成功了
- 异步发送需要接收回执并由客户端再判断一次是否发送成功。
生产者代码:(增添了回调函数)
public class JmsProduce_AsyncSend {
private static final String ActiveMQ_URL = "tcp://192.168.188.131:61616";
private static final String QUEUE_NAME = "queue_AsyncSend";
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(QUEUE_NAME);
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
TextMessage textMessage;
for (int i = 1; i <= 3; i++) {
textMessage = session.createTextMessage("msg--->" + i);
textMessage.setJMSMessageID(UUID.randomUUID().toString().substring(0, 6)+"_msg");
String msgId = textMessage.getJMSMessageID();
activeMQMessageProducer.send(textMessage, new AsyncCallback() {
@Override
public void onSuccess() {
System.out.println(msgId + " has been sent successfully.");
}
@Override
public void onException(JMSException e) {
System.out.println(msgId + " fail to send.");
}
});
}
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("***********消息发布到MQ完成");
}
}
12.3 延迟投递和定时投递
12.3.1 官网说明
四大属性
12.3.2 案例演示
-
要在activemq.xml中配置
schedulerSupport
属性为true
重启:./bin/activemq restart
-
Java代码里面封装的辅助消息类型:ScheduledMessage
代码
public class JmsProduce_DelayAndSchedule {
private static final String ActiveMQ_URL = "tcp://192.168.188.131:61616";
private static final String QUEUE_NAME = "delay";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageProducer messageProducer = session.createProducer(queue);
long delay = 3 * 1000;
long period = 4 * 4000;
int repeat = 5;
for (int i = 1; i <= 3; i++) {
TextMessage textMessage = session.createTextMessage("msg--->" + i);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
messageProducer.send(textMessage);
}
// 9. 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("***********消息发布到MQ完成");
}
}
12.4 分发策略
12.5 ActiveMQ消息重试机制
12.5.1 面试题
具体哪些情况会引发消息重发
- Client用了transactions且在session中调用了rollback
- Client用了transactions且在调用commit()之前关闭或者没有commit
- Client再CLIENT_ACKNOWLEDGE的传递模式下,session中调用了recover()
请说说消息重发时间间隔和重发次数
间隔:1s
次数:6
每秒发6次
有毒消息Poison ACK
一个消息被redelivedred超过默认的最大重发次数(默认6次)时,消费的回个MQ发一个“poison ack”表示这个消息有毒,告诉broker不要再发了。这个时候broker会把这个消息放到DLQ(私信队列)。
12.5.2 官网
12.5.3 属性说明
12.5.4 代码验证
使用带事务的消费者,但不调用commit(),执行(1(本地)+6(重复机会))7次之后,收不到消息,查看控制台:
12.5.5 修改重复次数等参数
JmsConsumer_Redelivery修改,添加下面代码即可。
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
redeliveryPolicy.setMaximumRedeliveries(3);
activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);
12.5.6 整合Spring后如何使用,假如工作中有需要
12.6 死信队列
12.6.1 官网
12.6.2 是什么
Active MQ中引入了==“死信队列”( Dead Letter Queue)的概念==。即一条消息再被重发了多次后(默认为重发6次 redelivery Counter=6),将会被 Activemq移入“死信队列”。开发人员可以在这个 Queue中查看处理出错的消息,进行人工干预。
12.6.3 死信队列的使用:处理失败的消息
- 一般生产环境中在使用MQ的时候设计两个队列:一个是核心业务队列,一个是死信队列。
- 核心业务队列,就是比如上图专门用来让订单系统发送订单消息的,然后另外一个死信队列就是用来处理异常情况的。
- 假如第三方物流系统故障了此时无法请求,那么仓储系统每次消费到一条订单消息,尝试通知发货和配送都会遇到对方的接口报错。此时仓储系统就可以把这条消息拒绝访问或者标志位处理失败。一旦标志这条消息处理失败了之后,MQ就会把这条消息转入提前设置好的一个死信队列中。然后你会看到的就是,在第三方物流系统故障期问,所有订单消息全部处理失败,全部会转入死信队列。然后你的仓储系统得专门有一个后台线程,监控第三方物流系统是否正常,能否请求的,不停的监视。一且发现对方恢复正常,这个后台线程就从死信队列消费出来处理失败的订单重新执行发货和配送的通知逻辑。
12.6.4 activemq死信队列的配置介绍
- SharedDeadLetterStrategy
将所有的Deadletter保存在一个共享的队列中,这是Activemq broker端默认的策略共享队列默认为“ ActiveMQ.DLQ”,可以通过“ deadLetterQueue”属性来设定。
- IndividualDeadLetterStrategy
把Deadletter放入各自的死信通道中
对于Queue而言,死信通道的前级默认为“ ActiveMQ.DLQ.Queue”;
对于Topic而言,死信通道的前级默认为“ Activemc.DLQ. Topic.”;
比如队列Order,那么它对应的死信通道为“Activemc.DLQ.Queue.Order”。
我们使用“queue Prefix”“ topicPrefix”米指定上述前缀。
默认情况下,无论是Topic还是Queue, broker将使用Queue来保存DeadLeader.即死信通道通常为Queue;不过开发者也可以指定为Topic。
将队列Order中出现的Deadletter保存在DLQ.Order中,不过此时DLQ.Order为Topic。
属性**“useQueueForTopicMessages”,此值表示是否将Topic的Deadletter保存在Queue中,默认为true**
- 配置案例
- 自动删除过期消息
有时需要直接删除过期的消息而不需要发送到死信队列中,“processExpired”表示是否将过期消息放入死信队列,默认为true
- 存放非持久消息到死信队列中
默认情况下, Activemq不会把非持久的死消息发送到死信队列中。
processionNonPersistent
表示是否将“非持久化”消息放入死信队列,默认为false
。
非持久性如果你想把非持久的消息发送到死队列中,需要设置属性processNonPersistent=true
12.7 如果保证消息不被重复消费呢?幂等性问题
网络延迟传输中,会造成进行MQ重试中,在重试过程中,可能会造成重复消费。
如果消息是做数据库的插入操作,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避兔数据库出现脏数据。
如果上面两种情况还不行,准备一个第三服务方来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将**<id, message>**以K-V形式写入redis。那消费者开始消费前,先去redis
中查询有没消费记录即可。