1.トランザクションメッセージの発信元
1.ケース
公式ショッピングケースの引用:
Xiao Mingが100元のアイテムを購入すると、アカウントから100元が差し引かれ、ダウンストリームポイントシステムがXiaoMingのアカウントに100ポイントを追加するようにする必要があります。アカウントシステムとポイントシステムは2つの独立したシステムであり、1つは100元削減し、もう1つは100ポイント増やす必要があります。以下に示すように:
2.問題
-
アカウントサービスの控除は成功し、通知ポイントシステムも成功しましたが、ポイントが増えると失敗し、データに一貫性がありませんでした。
-
アカウントサービスの控除は成功しましたが、通知ポイントシステムが失敗したため、ポイントは増加せず、データに一貫性がありません。
3.スキーム
最初の問題に対するRocketMQの解決策は、消費が失敗した場合、自動的に再試行することです。数回の再試行後に消費が失敗した場合、この状況は、デッドレターキューに入れてから手動で確認するなど、手動で解決する必要があります。原因とそれに対処します。
2番目の問題に対するRocketMQの解決策は、お金の控除に成功したが、mqへのメッセージの書き込みに失敗した場合、RocketMQはメッセージをロールバックします。この時点で、控除操作をロールバックすることもできます。
第二に、トランザクションメッセージの原則
1.原理図
2.詳細なプロセス
1.プロデューサーはハーフメッセージ(ハーフメッセージ)をブローカーに送信します。
なぜハーフメッセージと呼ばれ、わかりにくいのか、本当に文句を言いたいのですが、実はこれは準備メッセージ、送信済みメッセージです。
-
ハーフメッセージが正常に送信された後、ローカルトランザクションが実行されます。
-
ローカルトランザクションが正常に実行された場合はcommitを返し、実行が失敗した場合はロールバックを返します。(これは、トランザクションメッセージのコミットまたはロールバックのコールバックメソッドで開発者自身が決定します)
プロデューサーは前のステップのコミットまたはロールバックをブローカーに送信します。ここには2つの状況があります。
1.ブローカーがコミット/ロールバックメッセージを受信した場合:
-
コミットが受信された場合、ブローカーはトランザクション全体が正常であり、実行が成功したと見なします。次に、メッセージは消費のためにコンシューマー側に送信されます。
-
ロールバックが受信された場合、ブローカーはローカルトランザクションの実行が失敗したと見なし、ブローカーはハーフメッセージを削除してコンシューマーに送信しません。
2.ブローカーがメッセージを受信しない場合(ローカルトランザクションの実行が突然ダウンした場合、同等のローカルトランザクションの実行結果は不明を返し、ブローカーが確認メッセージを受信しなかったかのように扱われます)。
-
ブローカーは定期的にローカルトランザクションの実行結果を確認します。確認結果がローカルトランザクションが実行された場合はコミットを返し、そうでない場合はロールバックを返します。
-
プロデューサーのバックチェックの結果はブローカーに送信されます。ブローカーがコミットを受信した場合、ブローカーはトランザクション全体が正常に実行されたと見なします。ロールバックの場合、ブローカーはローカルトランザクションが失敗したと見なし、ブローカーはハーフメッセージを削除してコンシューマーに送信しません。ブローカーがバックチェックの結果を受け取らない場合(または不明な場合)、ブローカーは定期的にバックチェックを繰り返して、最終的なトランザクション結果が見つかるようにします。時間間隔と繰り返されるバックチェックの数の両方を構成できます。
3、トランザクションメッセージ実現プロセス
1.実装プロセス
簡単に言うと、トランザクションメッセージはコールバック関数を持つリスナーです。コールバック関数では、アカウントに-100元を与えてから、ポイントのmqにメッセージを送信するなどのビジネスロジック操作を実行します。アカウント-100が成功した場合、およびメッセージがmqに正常に送信された場合、メッセージステータスはコミットに設定されます。この時点で、ブローカーは半分のメッセージを実際のトピックに送信します。最初は、セミメッセージキューに送信されましたが、実際のトピックキューには存在しませんでした。コミットを確認して初めて転送されます。
2.是正計画
中断またはその他のネットワーク上の理由でトランザクションがすぐに応答しない場合、RocketMQはそれをUNKNOWとして扱います。RocketMQトランザクションメッセージも解決策を提供します。トランザクションメッセージのトランザクションステータスを定期的に照会します。これもコールバック関数です。補正はその中で行うことができます。補正ロジックの開発者は自分で書き込みます。成功すると、コミットに戻り、完了します。
4、トランザクションメッセージのコード例
1.コード
package com.chentongwei.mq.rocketmq;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.Date;
/**
* Description:
*
* @author TongWei.Chen 2020-06-21 11:32:58
*/
public class ProducerTransaction2 {
public static void main(String[] args) throws Exception {
TransactionMQProducer producer = new TransactionMQProducer("my-transaction-producer");
producer.setNamesrvAddr("124.57.180.156:9876");
// 回调
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
LocalTransactionState state = null;
//msg-4返回COMMIT_MESSAGE
if(message.getKeys().equals("msg-1")){
state = LocalTransactionState.COMMIT_MESSAGE;
}
//msg-5返回ROLLBACK_MESSAGE
else if(message.getKeys().equals("msg-2")){
state = LocalTransactionState.ROLLBACK_MESSAGE;
}else{
//这里返回unknown的目的是模拟执行本地事务突然宕机的情况(或者本地执行成功发送确认消息失败的场景)
state = LocalTransactionState.UNKNOW;
}
System.out.println(message.getKeys() + ",state:" + state);
return state;
}
/**
* 事务消息的回查方法
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
if (null != messageExt.getKeys()) {
switch (messageExt.getKeys()) {
case "msg-3":
System.out.println("msg-3 unknow");
return LocalTransactionState.UNKNOW;
case "msg-4":
System.out.println("msg-4 COMMIT_MESSAGE");
return LocalTransactionState.COMMIT_MESSAGE;
case "msg-5":
//查询到本地事务执行失败,需要回滚消息。
System.out.println("msg-5 ROLLBACK_MESSAGE");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
});
producer.start();
//模拟发送5条消息
for (int i = 1; i < 6; i++) {
try {
Message msg = new Message("transactionTopic", null, "msg-" + i, ("测试,这是事务消息! " + i).getBytes());
producer.sendMessageInTransaction(msg, null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2.結果
msg-1,state:COMMIT_MESSAGE
msg-2,state:ROLLBACK_MESSAGE
msg-3,state:UNKNOW
msg-4,state:UNKNOW
msg-5,state:UNKNOW
msg-3 unknow
msg-3 unknow
msg-5 ROLLBACK_MESSAGE
msg-4 COMMIT_MESSAGE
msg-3 unknow
msg-3 unknow
msg-3 unknow
msg-3 unknow
3.コントロールコンソール
4.結果分析
-
msg-1とmsg-4のみが正常に送信されました。msg-1が最初に成功し、msg-4はバックチェック後にのみ成功したため、msg-4はmsg-1よりも進んでいます。時系列の逆順で提供されます。
-
最初に、5つのメッセージに対応する5つの結果を出力します
msg-1、state:COMMIT_MESSAGE
msg-2、state:ROLLBACK_MESSAGE
msg-3、state:UNKNOW
msg-4、state:UNKNOW
msg-5、state:UNKNOW
-
次にレビューを入力すると、msg-3はまだ不明であり、msg-5はロールバックされ、msg-4はトランザクションを送信しました。したがって、この時点でmsg-4はコントロールコンソールに表示されます。
-
しばらくして、もう一度msg-3をチェックしたところ、まだわからないことがわかったので、チェックを続けました。
時間間隔とバックチェックの数は構成可能です。デフォルトでは、チェックバックが15回失敗すると、メッセージは破棄されます。
5、質問
質問:Springトランザクションと通常の分散トランザクションは良くありませんか?Rocketmqの業務は不要ですか?
MQはデカップリングに使用されます。以前は、分散トランザクションはアカウントシステムとポイントシステムを直接操作していました。しかし、その2つは強い結合の存在であり、mqが途中に挿入されると、アカウントシステムは操作後にmqにメッセージを送信し、送信が成功することが保証されている限り、送信されます。 、および送信が失敗した場合、送信はロールバックされます。そして、分散トランザクションにRocketMQを使用している人はかなりたくさんいます。
6、シーケンスメッセージの説明
1。概要
RocketMQメッセージはトピックキューに格納され、キュー自体はFIFO(First Int First Out)キューです。したがって、単一のキューで順序を保証できます。
ただし、問題は、トピックにN個のキューがあることです。作成者の設計の利点も明らかです。クラスタリングと負荷分散の特性を自然にサポートし、各キューに大量のデータを均等に分散します。10個のメッセージを同じトピック。、これらの10個のメッセージは、トピックの下のすべてのキューに自動的に分散されるため、消費するときに、どのキューが最初に消費され、どのキューが後で消費されるかは必ずしも必要ではなく、無秩序な消費につながります。
2.グラフィック
3.再度分析します
プロデューサーは、4つのメッセージm1、m2、m3、およびm4をトピックに送信します。トピックには4つのキューがあります。独自の負荷分散戦略により、4つのキューのそれぞれに1つのメッセージが格納されます。キュー1に格納されたm1、キュー2に格納されたm2、キュー3に格納されたm3、およびキュー4に格納されたm4。コンシューマはマルチスレッド消費を消費するため、送信の順序など、どのキューまたはメッセージが最初に消費されるかを保証できません。 m1、m2、m3、m4ですが、コンシューマーはコンシューマー内の複数のスレッドによって消費されるため、最初にqueue4キューのm4を消費し、次にm1を消費する可能性があります。
7つのシーケンシャルメッセージソリューション
1.オプション1
簡単に言うと、問題の鍵は、複数のキューにメッセージがあることです。消費すると、どのキューのメッセージが最新かわかりません。メッセージを送信するときに順序を確認したい場合は、メッセージをキューに送信してから、消費するときに、そのキューだけにメッセージがあり、キューはFIFOであるため、最初に先入れ先出しで、通常の消費になります。終わった。
非常に完璧な。また、RocketMQは、メッセージの送信時にキューを選択するためのAPI(MessageQueueSelector)も提供します。コードに直接移動します。
2.コード1
2.1。プロデューサー
import java.util.List;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
/**
* 消息发送者
*/
public class Producer5 {
public static void main(String[] args)throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("my-order-producer");
producer.setNamesrvAddr("124.57.180.156:9876");
producer.start();
for (int i = 0; i < 5; i++) {
Message message = new Message("orderTopic", ("hello!" + i).getBytes());
producer.send(
// 要发的那条消息
message,
// queue 选择器 ,向 topic中的哪个queue去写消息
new MessageQueueSelector() {
// 手动 选择一个queue
@Override
public MessageQueue select(
// 当前topic 里面包含的所有queue
List<MessageQueue> mqs,
// 具体要发的那条消息
Message msg,
// 对应到 send() 里的 args,也就是2000前面的那个0
// 实际业务中可以把0换成实际业务系统的主键,比如订单号啥的,然后这里做hash进行选择queue等。能做的事情很多,我这里做演示就用第一个queue,所以不用arg。
Object arg) {
// 向固定的一个queue里写消息,比如这里就是向第一个queue里写消息
MessageQueue queue = mqs.get(0);
// 选好的queue
return queue;
}
},
// 自定义参数:0
// 2000代表2000毫秒超时时间
0, 2000);
}
}
}
2.2。消費者
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
* Description:
*
* @author TongWei.Chen 2020-06-22 11:17:47
*/
public class ConsumerOrder {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer");
consumer.setNamesrvAddr("124.57.180.156:9876");
consumer.subscribe("orderTopic", "*");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
System.out.println(new String(msg.getBody()) + " Thread:" + Thread.currentThread().getName() + " queueid:" + msg.getQueueId());
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.println("Consumer start...");
}
}
2.3、出力結果
Consumer start...
hello!0 Thread:ConsumeMessageThread_1 queueid:0
hello!1 Thread:ConsumeMessageThread_1 queueid:0
hello!2 Thread:ConsumeMessageThread_1 queueid:0
hello!3 Thread:ConsumeMessageThread_1 queueid:0
hello!4 Thread:ConsumeMessageThread_1 queueid:0
非常に完璧で整然とした出力!
3.状況2
たとえば、新しい要件:すべての未払いの注文をqueue1に入れ、有料の注文をqueue2に入れ、異常な支払いの注文をqueue3に入れます。次に、消費するときに各キューが正しいことを確認する必要があります。、queue1を消費できず、直接queue2。キューを1つずつ消費する必要があります。
現時点では、メッセージを送信するときにカスタムパラメータargを使用することをお勧めします。メッセージ本文には支払いステータスが含まれている必要があります。未払いと判断された場合は、queue1などを選択します。これにより、各キューに同じ状態のメッセージのみが含まれるようになります。そのため、現在、コンシューマーは複数のスレッドによって消費されていますが、これらは故障している必要があります。3つのキューがランダムに消費されます。解決策はより単純で、コンシューマー側のスレッド数を直接1に変更して、キューがFIFOになるようにし、コンシューマー側が1つずつ消費します。RocketMQは、次の2つの文でそのようなAPIも提供します。
// 最大线程数1
consumer.setConsumeThreadMax(1);
// 最小线程数
consumer.setConsumeThreadMin(1);