前言
现在大型分布式系统都在运用消息队列,这完全得益于消息队列的优势,消息队列的优势无非6个字,解耦、异步、削峰。
(1)解耦
传统模式:
传统模式的缺点:
系统间耦合性太强,如上图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!
中间件模式:
中间件模式的的优点:
将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。
(2)异步
中间件模式:
中间件模式的的优点:
将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度。
(3)削峰
中间件模式:
中间件模式的的优点:
系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。
RocketMQ基础
RocketMQ物理部署结构
RocketMQ逻辑部署结构
RocketMQ的适合发送方式和接受消息
不同的消费者
根据不同的读取控制,消费者可分为两种类型。一个是DefaultMQPushConsumer,由系统控制的读取操作。另一个是DefaultMQPullConsumer,读取的大部分功能由使用者自主控制。
DefaultMQPushConsumer的处理流程
消息的处理逻辑在pullMessage这个函数的pullCallBack中,在Pull函数有个switch语句,根据从Broker返回的消息作相应的处理。
DefaultMQPushConsumer的流程控制
PushConsumer的核心还是Pull方式,所以采用这种方式的客户端能够根据自身的处理速度调整获取信息的操作。因为采用多线程处理方式实现,流量控制的方面比单线程复杂很多。
PushConsumer有个线程池,消息处理逻辑在各个线程里同时执行。
this.consumeExecutor = new ThreadPoolExecutor(
this.defaultMQPushConsumer.getConsumeThreadMin(),
this.defaultMQPushConsumer.getConsumeThreadMax(),
1000 * 60,
TimeUnit.MILLISECONDS,
this.consumeRequestQueue,
new ThreadFactoryImpl("ConsumeMessageThread_"));
Pull获得的消息,如果提交到线程池里执行,很难监控和控制,比如如何获得当前消息堆积的数量?如何处理某些消息?如何延迟处理某些消息。RocketMq定义了一个快照类ProcessQueue。每个Message Queue都会有对应的ProcessQueue对象都会有对应的ProcessQueue对象,保存这个MessageQueue消息处理状态的快照。
ProcessQueue对象里主要的内筒是是一个TreeMap和一个读写锁。TreeMap里以MessageQueue的offset作为Key,以内容的引用为value,保存了所有从MessageQueue获取到的,还没有处理的消息。读写锁控制了对TreeMap对象的并发访问。
DefaultMQPushConsumer实例分析
public class PullConsumer {
//维护offsetstore
private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
consumer.start();
//拉取topicid下所有的消息队列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");
for (MessageQueue mq : mqs) {
System.out.printf("Consume from the queue: %s%n", mq);
SINGLE_MQ:
while (true) {
try {
PullResult pullResult =
//从broker上根据偏移量以及个数拉取消息消费
consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
System.out.printf("%s%n", pullResult);
//维护offsetstore 存储下一个消费消息的偏移量
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
consumer.shutdown();
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSE_TABLE.get(mq);
if (offset != null)
return offset;
return 0;
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
OFFSE_TABLE.put(mq, offset);
}
}
实例代码的处理逻辑是读取某个Topic下所有的内容,主要处理的三件事情:
(1)获取MessageQueue并遍历
(2)维护offstore
(3)根据不同的消息状态做不同的处理
不同的生产者
生产者向消息队列写入消息,不同的业务场景需要生产者采用不同的写入策略。比如同步发送,异步发送,延迟发送,发送事务消息等。
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
for (int i = 0; i < 1000; i++) {
try {
/*
* Create a message instance, specifying topic, tag and message body.
*/
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
/*
* Call send message to deliver message to one of brokers.
*/
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
producer.shutdown();
}
}
发送消息要经过五个步骤:
1)设置producer的GroupName。
2)设置InstanceName,当一个Jvm需要启动多个Producer的时候,通过不通的InstanceName来区分,不设置的话系统使用默认名称“DEFAULT”.
3)设置发送失败次数,当网络出现异常的时候,这个次数影响消息的重复投递次数。想保证不掉消息,可以设置多重试几次。
4)设置nameserver地址
5)组装并发送消息
消息队列具体使用案列
下面介绍项目具体应用,因为我们的项目中用的阿里的消息队列。
消息队列的maven依赖:
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>ons-client</artifactId>
</dependency>
初始化消息的生产者,消费者:
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ProducerBean liquidatorCounterofferProducer() {
ProducerBean producerBean = new ProducerBean();
Properties properties = new Properties();
properties.put(PropertyKeyConst.ProducerId, liquidatorCounterofferProperty.getProducerid());
properties.put(PropertyKeyConst.AccessKey, aliyunProperty.getAccesskey());
properties.put(PropertyKeyConst.SecretKey, aliyunProperty.getSecretkey());
producerBean.setProperties(properties);
return producerBean;
}
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ConsumerBean liquidatorCounterofferListener() {
ConsumerBean consumerBean = new ConsumerBean();
Properties properties = new Properties();
properties.put(PropertyKeyConst.ConsumerId, liquidatorCounterofferProperty.getConsumerid());
properties.put(PropertyKeyConst.AccessKey, aliyunProperty.getAccesskey());
properties.put(PropertyKeyConst.SecretKey, aliyunProperty.getSecretkey());
consumerBean.setProperties(properties);
Subscription subscription = new Subscription();
subscription.setTopic(liquidatorCounterofferProperty.getTopicid());
subscription.setExpression("*");
Map<Subscription, MessageListener> map = Maps.newHashMap();
map.put(subscription, new LiquidatorCounterofferListener());
consumerBean.setSubscriptionTable(map);
return consumerBean;
}
发送消息具体实现:
// 封装消息体
Message message = new Message(
// Message Topic
MQ_LIQUIDATOR_COUNTEROFFER_TOPIC_ID,
// Message Tag 可理解为Gmail中的标签,对消息进行再归类,方便Consumer指定过滤条件在MQ服务器过滤
MQ_LIQUIDATOR_COUNTEROFFER_TAG,
// Message Body 可以是任何二进制形式的数据, MQ不做任何干预
// 需要Producer与Consumer协商好一致的序列化和反序列化方式
msg.getBytes());
// 设置代表消息的业务关键属性,请尽可能全局唯一
// 以方便您在无法正常收到消息情况下,可通过MQ 控制台查询消息并补发
// 注意:不设置也不会影响消息正常收发
message.setKey(msg);
// 发送消息,只要不抛异常就是成功
try {
SendResult sendResult = liquidatorCounterofferProducer.send(message);
logger.info("********消息队列成功 >> alipayRiskMerchant >> msg = {}, message = {}, sendResult = {}", msg, message, JSON.toJSONString(sendResult));
return true;
} catch (ONSClientException e) {
logger.error("******消息队列失败 >> alipayRiskMerchant >> msg = {}, message = {}, error = {}", msg, message, ExceptionUtils.getRootCauseStackTrace(e));
return false;
}
消费者具体实现逻辑:
//消费者订阅监听的具体实现
public Action consume(Message message, ConsumeContext consumeContext) {
// 取出消息
String msg;
try {
msg = new String(message.getBody(), "utf-8");
} catch (UnsupportedEncodingException e) {
//监听读取消息异常
return Action.CommitMessage;
}
//dosomething 解析消息数据
try {
//解析数据具体调用 http请求第三方
String result = HttpUtil.getInstance(NOTIFY_TIMEOUT).post(notifyUrl, map);
if(!"Success".equalsIgnoreCase(result)){
//消费失败 消费重试
return Action.ReconsumeLater;
}
//消息处理成功 返回消费成功
return Action.CommitMessage;
} catch (Exception e) {
//消费失败 消费重试
return Action.ReconsumeLater;
}
}