「rocketmq実用的な原則と分析」からの抜粋
生産者と消費者が生産者は、消費者がメッセージキューからデータを読み込み、メッセージ・キューにデータを書き込み、二つの重要な役割のメッセージ・キューです。ほとんどのRocketmqユーザーが唯一の懸念の生産者と消費者を必要とし、この記事では、生産者と消費者とそのオフセットとログの特性に焦点を当てます
消費者は、1つのDefaultMQPushConsume、システム制御による読み出し動作、着信メッセージを受信した後、自動呼処理方法、他方はDefaultMQPullConsume、ある機能のほとんどの読み出し動作であり、二つの異なるタイプに分けることができますユーザーによる自律制御。
DefaultMqPushConsume使用
プッシュモードでは、システムは、自動的に次のように、オフセット保存、負荷が自動的に新しいDefaultMqPushConsume後に添加バランシング自動的に、メッセージを処理するための機能を処理するメッセージを受信した後呼ぶ、主要パラメータと受信メッセージ機能のような優れた品種です。
package rocketmq.day02;
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
import com.alibaba.rocketmq.common.protocol.heartbeat.MessageModel;
import java.io.UnsupportedEncodingException;
/**
* @author heian
* @create 2019-12-09-8:12 上午
* @description
*/
public class QuickStart {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("unique_consume_group_name");
consumer.setNamesrvAddr("192.168.142.133:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.setMessageModel(MessageModel.CLUSTERING);//默认是集群模式
consumer.subscribe("topicName",null);
consumer.registerMessageListener((MessageListenerConcurrently) (listMsg, consumeConcurrentlyContext) -> {
byte[] body = listMsg.get(0).getBody();
try {
String ms = new String(body,"utf-8");
System.out.println(Thread.currentThread().getName()+"收到消息:" + ms);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
}
}
三つのパラメータ、コンシューマ・グループ名consumeGroupName、アドレスポートネームサーバ、トピック名トピックを設定する必要がDeafultMqPushConsume。
1)並行性を向上させると共に、消費者の消費者団体の複数の消費者グループの名前、および一般メッセージMessageModelモードと一緒に使用、パターンの一般二種類、及びBoradcastingセットクラスタリングをブロードキャスト
2)クラスタモデルの消費者は、サブスクリプション・メッセージの一部のみを消費する消費者のグループ名の下に同じことが、個人消費の同じコンシューマ・グループ名は、負荷を達成するように、トピックのサブスクリプションのニュースコンテンツ全体の合計ですバランス
3)消費者のグループ名の消費者がすべてのニューストピックを購読することができ、各コンシューマ以下同じブロードキャストモードでは、メッセージが複数回配信されるされ、より多くの消費者の消費しました
IP2、ポート:PORT2をIP1:4)ネームサーバがアドレスポートの複数を満たすことができ、それによってような単一障害点を排除し、セミコロンで区切ら
4)トピック名は、メッセージの種類を示すために使用される、あなたが事前に作成する必要があります。あなたはニュースコンテンツの話題を消費しない場合consume.subsrcibe(「ます。topicName」、「TAG1 ||タグ2 || TAG3」)、消費者はこれだけ話題にTAG1 2 3をサブスクライブする必要が指示がある場合は、タグに基づいてフィルタリングすることができますニュース、または*ヌルフィルがトピックのすべてのメッセージのサブスクリプションを表す場合
DefaultMqPushConsumeプロセスフロー
DefaultMqPushConsume主な機能DefaultMqPushConsumerImplクラス、メッセージ処理ロジックはPullCallback pullMessageこの関数コールバッククラスで、このクラスは、リターンへのメッセージブローカーの種類に応じて使用したいとの契約を行う、switch文がありますが、なぜ次のプッシュモードを見ることができますpullRequestは、それを使うのか?これは、長押しポーリングによって到達され、両方の利点は、長いポーリングを引いて、リアルタイムのプッシュがあります。
PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
if (pullResult != null) {
pullResult =
DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(
pullRequest.getMessageQueue(), pullResult, subscriptionData);
switch (pullResult.getPullStatus()) {
case FOUND:
long prevRequestOffset = pullRequest.getNextOffset();
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
long pullRT = System.currentTimeMillis() - beginTimestamp;
DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(
pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullRT);
long firstMsgOffset = Long.MAX_VALUE;
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
else {
firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(
pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(),
pullResult.getMsgFoundList().size());
boolean dispathToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(//
pullResult.getMsgFoundList(), //
processQueue, //
pullRequest.getMessageQueue(), //
dispathToConsume);
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
}
else {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
}
if (pullResult.getNextBeginOffset() < prevRequestOffset//
|| firstMsgOffset < prevRequestOffset) {
log.warn(
"[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",//
pullResult.getNextBeginOffset(),//
firstMsgOffset,//
prevRequestOffset);
}
break;
case NO_NEW_MSG:
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case NO_MATCHED_MSG:
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case OFFSET_ILLEGAL:
log.warn("the pull request offset illegal, {} {}",//
pullRequest.toString(), pullResult.toString());
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
pullRequest.getProcessQueue().setDropped(true);
DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
@Override
public void run() {
try {
DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(
pullRequest.getMessageQueue(), pullRequest.getNextOffset(), false);
DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest
.getMessageQueue());
DefaultMQPushConsumerImpl.this.rebalanceImpl
.removeProcessQueue(pullRequest.getMessageQueue());
log.warn("fix the pull request offset, {}", pullRequest);
}
catch (Throwable e) {
log.error("executeTaskLater Exception", e);
}
}
}, 10000);
break;
default:
break;
}
}
}
@Override
public void onException(Throwable e) {
if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("execute the pull request exception", e);
}
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
PullTimeDelayMillsWhenException);
}
}
プッシュモードは、サーバがメッセージを受信した後に終了し、メッセージがリアルタイムハイにアクティブなクライアントにプッシュされます。サーバー側のサービスを提供するためのキューのために、プッシュプッシュ方法の使用は多くの欠点があります。
- これにより、サーバー側のパフォーマンスに影響を与え、サーバ側の負荷を増やします
- クライアント側は、様々な潜在的な問題の存在の上にプッシュされたメッセージのSERVER1を処理できない場合、クライアント側の処理能力は、サーバ側、クライアント側の状態制御から、変化します
プル実施形態は、離れて、サーバからのメッセージクライアント側円ドライブで、彼は特定のメッセージに私を得たことを手にクライアント側のイニシアチブは、その後、適切に処理して撮影しました。プルアプローチが問題です。
- 私の悪いニュースサイクル間隔の設定を手に入れた、間隔が「ビジー待機」、資源の廃棄物の状態であることには短すぎます。
- 、各時間間隔が長すぎるメッセージサーバを終了する時間を引いて、タイムリーに存在しないかもしれません。
交配クライアントとサーバー側のロングポーリング方式は、両方の長所を持って引っ張って、だけでなく、リアルタイム性を確保する目的を達成します。この期間に、彼はまた、ブローカーの役割を設定しrequest.setSuspendTimeOutMills(brokerSuspendMaxTimeMills)のimplソースは、デフォルトではこれは新しい情報が存在しない場合に、ブローカー、ニュースはすぐに返されますブロックされるがあること15S、ノートで、最長ブロック時間です。
private static final long BrokerSuspendMaxTimeMillis = 1000 * 15;
新しいメッセージ・キューを返すために熱心ではないではありませんが、各waitForRunning期間(デフォルトをループのステータスを表示し続ける場合は、ソースのブローカーから見ることができる(図示せず)、サーバは、要求を受信5秒)、その後、確認してください。デフォルトでは、ブローカは新しいメッセージ、第三チェックされていないとき、待機時間はbrokerSuspendMaxTimeMills内部の要求は、それが空の結果を返し超えます。直接呼び出しnotifyMessageArriving機能は、プロセスを待ってから、要求の結果を返し、ブローカは新しいメッセージを受信しました。コア「ロングポーリング」であり、それは、この時点ですぐに消費するように既存の接続を使用してメッセージを返し、新しいメッセージが到着し、短い時間の経過ブローカークライアント側の保留ライブ要求。「ロングポーリング」構想は、まだ終わりでは休符は消費、メッセージのブローカータイムリーな大きなバックログは、積極的に消費するようにプッシュされません。
制限事項ロングポーリング要求がライブ必要なときにリソースが占有消費保留され、それがこのクライアントメッセージキューのシーンの制御された量に適しています。その後、我々は、それがプッシュモードまたはプルモードであるかどうかを確認することができます(ロングポーリング)pullRequestの方法を使用している、ちょうどプッシュモードは、メソッドサーバでのヘルプクライアントへ(ブローカーは継続的にポーリングを持っているでブローカーからプルメッセージを達成すぐにメッセージを返し、メッセージはまだサイクルので、空の結果)が返され何のニュースをで3回、15秒を5秒間隔でサーバーの状態を確認せず、ユーザーが達成するために必要とされる独自の方法を引っ張って絶えず循環していません。
フロー制御DefaultMqPushConsume
このセクションでは、このようにしてクライアントが処理速度に取得したメッセージの速度に応じて調整することができるように、PushConsume、コア又はプル方式をプッシュのフロー制御方式を解析します。それがあるので達成するためのアプローチをマルチスレッド、フロー制御は、シングルスレッドよりもはるかに複雑です。(クラスConsumeMessageOrderlyService実装ConsumeMessageService)
this.consumeExecutor = new ThreadPoolExecutor(//
this.defaultMQPushConsumer.getConsumeThreadMin(),//
this.defaultMQPushConsumer.getConsumeThreadMax(),//
1000 * 60,//
TimeUnit.MILLISECONDS,//
this.consumeRequestQueue,//
new ThreadFactoryImpl("ConsumeMessageThread_"));//修改线程后缀名
スレッドプールに直接提出された場合に引いてメッセージを取得するなど、実行を監視し、制御することは困難である:どのように蓄積量のメッセージを取得しますか?繰り返しにメッセージのいくつかを対処するには?特定のメッセージを処理遅延させる方法は?このクラスが実行されている場合、各メッセージキューは、オブジェクト対応processQueueが存在するであろう、これらの問題を解決するために、スナップショットクラスprocessQueueを与えrocketmq、状態のメッセージキューメッセージ処理スナップショットを保存しました。(Processqueneクラスのメンバ変数)
private final Logger log = ClientLogger.getLog();
private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock();
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
private volatile long queueOffsetMax = 0L;
private final AtomicLong msgCount = new AtomicLong();
.......
メインコンテンツProcessQueueターゲットは実際にメッセージがすべて保存されますが、メッセージを取得するために、メッセージキューから処理されていない内の値を渡すために、キーとしてメッセージキューを相殺するために、前者、ツリーマップとreadwritelockに格納され、読み書きロック制御ツリーマップの複数のスレッドへの同時アクセス。
processqueueオブジェクトを使用すると、フロー制御を次のように各プルは、フローを制御するには、次の3人の裁判官を行います前に、簡単かつ柔軟性、およびクライアントの要求がたくさんある:わずか
pushConsumerメッセージが取得していないが、プロセスの数(スレッドプールキュー)、メッセージの合計サイズ、オフセットスパン、あればプルメッセージに間隔をあけてセットのサイズを超えるよう、任意の値が、ようにフロー制御を達成することを決定します目的。またprocessQueueも消費の論理的な順序を達成するのに役立つことができます。
DefaultMQPullConsumer使用
使用DefaultMQPullConsumer様々なパラメータを設定するDefaultMqPushConsumeの必要性を使用するのと同じことが、書き込み機能は、メッセージが、いくつかの余分なものを行う必要性を処理します。org.apache.rocketmq.example.simpleパッケージを導入するために、ソースの例を引き継ぐしてください、次のコード(https://github.com/apache/rocketmq)
package rocketmq.day03;
import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageQueue;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author heian
* @create 2019-12-26-12:47 下午
* @description pull消费者 地址:https://github.com/apache/rocketmq/blob/master/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.java
*/
public class PullConsume {
//保存offset
private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consume_group");
consumer.setNamesrvAddr("192.168.0.102:9876");// 192.168.0.102
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicName");
for (MessageQueue mq : mqs) {
System.out.println("当前队列"+mq);
SINGLE_MQ:
while (true) {
try {
long start = System.currentTimeMillis();
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end-start));
System.out.println("拉取结果:"+pullResult);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
System.out.println("存储在内存:"+OFFSE_TABLE);
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);
}
}
メッセージを介して送信されている前に、mqAdminコンソールメッセージ、次のように:(あなたのブローカーが終わりを開始しなかった場合は、空のように見えます)
次のようにコンソール出力の考え方は次のとおりです。
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]
耗时:9
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8}
耗时:20989
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8}
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]
耗时:4
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=12]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12}
耗时:20005
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12}
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]
耗时:3
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8}
耗时:20007
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8}
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=0]
耗时:4
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=30, minOffset=0, maxOffset=30, msgFoundList=30]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=0]=30}
耗时:25005
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=30, minOffset=0, maxOffset=30, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=0]=30}
これは、メッセージキューが最初にメモリに保存されていない何のメモリ表情がなく、その後、ポーリングメッセージキューのセーブ・第二は、のようなエンド・メッセージ・ブローカを引っ張って、消費者が(何があった、ときにポーリング各キューを見つけることができます新しいメッセージ、デフォルトソース= 1000 * 10プライベート長いconsumerPullTimeoutMillisをブロック10秒)の上に 、 メッセージ・ブローカーの終わりは、またしばらくの間保持され、その後、メモリに格納されたメッセージは、分析的原則プルメッセージを参照してください。プル解析するソースを
パブリッククラスDefaultMQPullConsumerのメンバ変数:
- brokerSuspendMaxTimeMillis 20代デフォルトの最大タイムアウト長さが戻りPULL_NOT_FOUNDは、すぐにクライアントに返されていない場合Brokerの最後に保存されたファイルからのメッセージのルックアップの時間オフセットプルによると、ポーリングモードに懸濁し、しかしPullRequestHoldServiceスレッドに、 、そうでない場合はタイムアウトを返すことを発見した場合は5秒ごとにクライアントを引っ張って、メッセージをメッセージを引っ張って行きます。
- consumerTimeoutMillisWhenSuspend:30代デフォルトメッセージ全体引っ張っプロセス、サーバーの応答結果のタイムアウトのためにクライアントが待機を引っ張っ
- consumerPullTimeoutMillis:確立したネットワーク接続のタイムアウトのデフォルト10sがニュースを引きます
- messageModel:消費パターン、放送、クラスタ
- messageQueueListener : 业务消息监听器
- OffsetStore :消息消费进度管理器
- registerTopics :注册主题数
- allocateMessageQueueStrategy :队列分配器
- maxReconsumeTimes :最大消息重试次数,默认16次
(1)获取MessagQueue 并遍历
一个Topic包含多个MessagQueue,如果这个consume需要获取topic所有的消息,就要遍历所有的MessagQueue,如果有特殊情况也可以选定某些特定的MessagQueue 来读取消息
(2)维护Offsetstore
从一个MessagQueue里拉取消息的时候,需要传入offset参数,随着不断的读取消息,offset会不断增长。这个时候由用户负责把offset存储下来,根据具体的情况可以存到内存或者写入到磁盘或者数据库等
(3)根据不同的消息状态做不同的消息处理
拉取消息的请求发出后,会返回 FOUND NO_MATCHED_MSG NO_NEW_MSG OFFSET_ILLEAGER四种状态,需要根根据每个状态做不同的消息处理,比较重要的两个状态是FOUND NO_NEW_MSG,分别表示获取到消息和没有新的消息
但我觉得这上面拉取消息是不合理的,因为你拉取消息后还得在拉取一遍,直到消息返回状态为无此消息才跳出循环拉取下一条。
实际情况中可以把while true循环放到外层,达到无限循环的目的,不断循环去拉去消息队列的消息(会有新的消息)因此pullConsume需要用户自己处理遍历MessagQueue,所以pullConsume有更多的自主性和灵活性。
consume的启动 关闭流程
消息队列一般是提供一个不间断的持续性服务,consume在使用过程中如何才能更优雅的启动和关闭,确保消息不漏掉和重复消费呢?
consume分为push和pull两种方式,对于pullConsume来说使用者的主动权很高,可以根据实际情况去暂停 启动 和停止消费过程。需要注意的是offset的保存,要在程序的异常处理部分增加把offset写入磁盘方面的处理,记住了每个MessagQueue的offset,才能保证消息的准确性。
pushConsume在启动的时候会做各种配置的检查,然后连接nameserver获取topic信息,在启动的时候如果遇到异常,比如无法连接nameserver,程序仍然可以正常启动不报错。(在单机环境下可以测试下),那为什么不直接报错退出呢?这个和分布式设计有关,RocketMq集群可以有对歌nameserver broker,某个机器出了问题整体服务依然可用,所以在DefaultMQPushConsumer被设计成当发现某个连接异常时不立刻退出,而是不断尝试重新连接。可以进行这样一个测试,在DefaultMQPushConsumer正常运行的时候,手动kill调broker或者name server 过一会在启动,会发现DefaultMQPushConsumer不会出错退出,在服务恢复后可正常运行,在服务不可用的期间仅仅是在日志里报异常信息(对应的broker或者name server日志)。
如果需要在消费者启动的时候去报露错误,可以在consume.start()启动后调用,如下,其会抛出MQClientException异常(配置信息不准确或者当前服务不可用)
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicname");
不同类型的生产者
生产者向消息队列写数据,不同的业务场景需要生产者采取不同的策略。比如同步发送 异步发送 发送事物消息等,下面具体介绍。
package rocketmq.day01;
import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.remoting.exception.RemotingException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ProducerDemo {
public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("unique_producer_group__name");
producer.setNamesrvAddr("192.168.142.133:9876");
producer.start();
for (int i = 0; i < 1; i++) {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String format = sdf.format(date);
Message message = new Message("topicName", String.valueOf(i),format.getBytes());
SendResult sendResult= new SendResult();
try {
sendResult = producer.send(message);//有消息返回
} catch (RemotingException |MQBrokerException | InterruptedException e) {
System.out.println("消息发送失败:" + sendResult.getMsgId());
e.printStackTrace();
}
System.out.println("key:"+i + "消息的发送结果为:" + sendResult.toString() + "消息ID为:" + sendResult.getMsgId());
}
producer.shutdown();
}
}
发送消息经历五个步骤:
- 设置producer的GroupName
- 设置InstanceName 当一个jvm需要启动多个procuder的时候,通过设置不同的InstanceName来区分,不设置的话默认使用“DEFAULT"
- 设置发送失败重试次数,当网络出现异常的时候,这个次数影响消息的重复投递次数,想要保证消息不丢,可以设置多重试
- 设置nameserver 地址
- 组装并发送
消息的发送有同步发送和异步发送两种,上面使用的是异步。消息发送返回的状态有:FLUSH_DISK_TIMEOUT FLUSH_SLAVE_TIMEOUT SLAVE_NOT_AVAILABKE SEND_OK 不同状态在不同的刷盘策略和不同的同步策略的含义是不同的。
- FLUSH_DISK_TIMEOUT:表示没有在规定的时间内完成刷盘(需要broker的刷盘策略被设置成SYNC_FLUSH才会报这个错)
- FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且broker 被设置成SYNC_MASTER方式,没有在设定时间内完成主从同步
- SLAVE_NOT_AVAILABKE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示主备方式下,并且broker被设置成SYNC_MASTER,但是没有找到被配置成slave的broker
- SEND_OK:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被同步到slave上?消息在slave上是否被写入磁盘?需要结合所配置的刷盘策略 主从策略来定,这个状态还可以简单的理解为,没有发生上面列出的三个问题状态就是SEND_OK
写一个高质量的生产者程序,重点在于发送结果的处理,要充分考虑各种异常,写清楚对应的处理逻辑。
(1)发送延迟消息
支持发送延迟消息,broker接受到消息后,延迟一段时间在处理,使得消息在规定的一段时间内生效,延迟消息的使用方法是创建mmessage对象时,调用setDelayTimeLevel(int level) 方法设置,目前延迟消息不支持任意设置,仅支持如下(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h),比如setDelayTimeLevel(3)表示延迟10s。
message.setDelayTimeLevel(3);
(2) 自定义消息发送规则
一个topic会有多个message queue,如果使用procuder的默认配置,这个procuder会轮流像各个message queue发送消息。consume在消费消息的时候,会根据负载均衡策略,消息被分配到对应的message queue,如果不经过特定的设置,某条消息被发到哪个message queue,被哪个consume消费是未知的。
如果业务需要我们把消息发送到指定的消息队列,那该怎么办?
package rocketmq.day03;
import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.MessageQueueSelector;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.common.message.MessageQueue;
import com.alibaba.rocketmq.remoting.exception.RemotingException;
import org.aspectj.weaver.ast.Var;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* @author heian
* @create 2019-12-30-11:24 上午
* @description 指定生产者往某一个特定的队列中发消息
*/
public class MyMessageQueueSelect implements MessageQueueSelector {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object orderKey) {
int id = Integer.parseInt(orderKey.toString());
int idMainIndex = id/100;
int size = mqs.size();
int index = idMainIndex%size;
return mqs.get(index);
}
public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("unique_producer_group__name");
MyMessageQueueSelect myMessageQueueSelect = new MyMessageQueueSelect();
producer.setRetryTimesWhenSendFailed(3);//重试次数
producer.setNamesrvAddr("192.168.142.133:9876");//多个用;分割
producer.start();
for (int i = 0; i < 1; i++) {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String format = sdf.format(date);
Message message = new Message("topicName", String.valueOf(i),format.getBytes());
SendResult sendResult= new SendResult();
try {
String orderKey = "";
sendResult = producer.send(message, myMessageQueueSelect, orderKey);
} catch (RemotingException | MQBrokerException | InterruptedException e) {
System.out.println("消息发送失败:" + sendResult.getMsgId());
e.printStackTrace();
}
System.out.println("key:"+i + "消息的发送结果为:" + sendResult.toString() + "消息ID为:" + sendResult.getMsgId());
}
}
}
发送消息的时候需要把MessageQueueSelector 的对象作为参数,使用public SendResult send(Message msg,MessageQueueSelector selector,Object arg)函数发送消息即可。在MessageQueueSelector的实现中,根据需要传入orderkey参数或者根据message消息内容确定把消息发到哪个message queue上,返回被选中的message queue
生产者源码
今天测试环境出现了一个bug,看报错信息如下com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl方法中的Validators.checkMessage(msg, this.defaultMQProducer);方法
if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
"the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
}
因为我的消息体是按照1000个发送的其大小在1兆多点,而使用平安内部框架观察源码最大限制是4兆,这就很奇怪为什么我消息体没超过限制却发生了这样的报错呢?com.alibaba.rocketmq.client.producer.DefaultMQProducer
private String createTopicKey = MixAll.DEFAULT_TOPIC;
private volatile int defaultTopicQueueNums = 4;
private int sendMsgTimeout = 3000;
private int compressMsgBodyOverHowmuch = 1024 * 4;
private int retryTimesWhenSendFailed = 2;
private boolean retryAnotherBrokerWhenNotStoreOK = false;
private int maxMessageSize = 1024 * 128; //平安的这里是1024*1024*4
private boolean unitMode = false;
而且看源码发现了一个比较有意思的语法,也是在com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
while(true){
lable122:{
String info;
if(times<timesTotal){
....
}
break lable122;
}
times++;
}
其实也很好理解,就是你在while里自定一个一个代码块,每次执行的时候你跳出的不是整个循环而是自己自定义的代码块,如果while中有多个代码块,二者是相互隔离的,如下:跳出的代码块不再执行
public static void main(String[] args) {
label1:
for (int i=0;i<=100;i++){
label2:
if (i>6){
System.out.println("label2="+i);
break label2;
}
if (i==10){
System.out.println("the end");
break label1;
}
label3:
if (i>8){
System.out.println("break label3");
break label3;
}
}
}
label2=7
label2=8
label2=9
break label3
label2=10
the end