目次
第二に、ソリューション[ローカルメッセージテーブルソリューション]
第三に、ソリューション[RocketMQトランザクションメッセージプログラム]
第四に、RocketMQは信頼できるメッセージの最終的な一貫したトランザクションを実現します
1.信頼できるメッセージの最終的な一貫性の概要
信頼性の高いメッセージ最終整合性スキームは、トランザクションイニシエーターがローカルトランザクションを完了してメッセージを送信するときに、トランザクション参加者(メッセージコンシューマー)がメッセージを受信してトランザクションを正常に処理できる必要があることを意味します。このスキームは、メッセージがトランザクション参加者に送信される限り、当事者の最終的な業務は一貫している必要があります。このプログラムは、次の図に示すように、メッセージミドルウェアを使用して完了します。
トランザクションイニシエーター(メッセージプロデューサー)はメッセージをメッセージミドルウェアに送信し、トランザクション参加者はメッセージミドルウェアから、トランザクションイニシエーターとメッセージミドルウェアの間、およびトランザクション参加者(メッセージコンシューマー)とメッセージミドルウェアの間でメッセージを受信しますすべての通信はネットワークを介して行われ、ネットワーク通信の不確実性は分散トランザクションの問題を引き起こします。したがって、信頼できるメッセージの最終的な一貫性により、次の問題を解決する必要があります。
[1]ローカルトランザクションとメッセージ送信のアトミック性:トランザクションイニシエーターは、ローカルトランザクションが正常に実行された後にメッセージを送信する必要があります。そうでない場合、メッセージは破棄されます。つまり、ローカルトランザクションとメッセージ送信のアトミック性を実現するには、両方が成功するか、両方が失敗します。ローカルトランザクションとメッセージ送信の原子性は、信頼できるメッセージの最終的な一貫性を実現するための重要な問題です。この種の操作を最初に試し、最初にメッセージを送信してからデータベースを操作してみましょう。この場合、メッセージが正常に送信され、データベース操作が失敗する可能性があるため、データベース操作と送信されるメッセージの整合性は保証されません。
begin transaction;
//1.发送MQ
//2.数据库操作
commit transation;
2番目の解決策は、最初にデータベース操作を実行してからメッセージを送信することです。この場合、問題はないようです。MQメッセージの送信に失敗すると、例外がスローされ、データベーストランザクションがロールバックされます。だが:
- それがある場合はタイムアウト例外、データベースはロールバックされますが、MQは、実際に正常に送信されてきた、その意志も不一致の原因。
- メッセージが正常に送信されると、トランザクションが最終的にコミットされたときにJVMが突然ハングし、トランザクションが正常にコミットされないため、2つのシステム間でデータに一貫性がなくなります。
begin transaction;
//1.数据库操作
//2.发送MQ
commit transation;
[2]トランザクション参加者が受信するメッセージの信頼性:トランザクション参加者は、メッセージキューからメッセージを受信できる必要があります。メッセージの受信に失敗した場合、メッセージを繰り返し受信できます。
[3]メッセージの繰り返し消費の問題:ステップ2の存在により、コンシューマーノードがタイムアウトしたが消費が成功した場合、メッセージミドルウェアはこの時点でメッセージを繰り返し配信し、メッセージの繰り返し消費をもたらします。メッセージの繰り返し消費の問題を解決するには、トランザクション参加者のメソッドの同一性を実現する必要があります。
第二に、ソリューション[ローカルメッセージテーブルソリューション ]
ローカルメッセージテーブルソリューションは、もともとeBayによって提案されました。このソリューションの中核は、ローカルトランザクションを通じてデータサービス操作とメッセージの一貫性を確保し、時限タスクを通じてメッセージミドルウェアにメッセージを送信し、メッセージがコンシューマーに正常に送信されるまで待機することです。メッセージを削除します。以下は、説明のためにポイントを送信するための登録の例です。次の例には、ユーザーサービスとポイントサービスの2つのマイクロサービスインタラクションがあり、ユーザーサービスはユーザーの追加を担当し、ポイントサービスはポイントの追加を担当します。
[インタラクションプロセスは次のとおりです] :[1]ユーザー登録:ユーザーサービスは、ローカル業務に新しいユーザーを追加し、「ポイントメッセージログ」を追加します。(ユーザーテーブルとメッセージテーブルは、ローカルトランザクションを通じて一貫性が保証されます。)以下は疑似コードです。この場合、ローカルデータベース操作と保存された統合メッセージログは同じトランザクション内にあり、ローカルデータベース操作とメッセージログの記録操作はアトミックです。
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
[ 2 ]スケジュールされたタスクスキャンログ:メッセージがメッセージキューに送信されるようにするにはどうすればよいですか?最初のステップの後、メッセージはメッセージログテーブルに書き込まれ、独立したスレッドを開始して、メッセージログテーブル内のメッセージを定期的にスキャンし、メッセージミドルウェアに送信できます。メッセージミドルウェアが送信が成功したことを報告した後、メッセージログは削除されます。スケジュールされたタスクが次のサイクルで再試行されるのを待ちます。
[3]消費者ニュース:消費者がニュースを消費できるようにするにはどうすればよいですか?ここでは、MQのack(メッセージ確認)メカニズムを使用できます。消費者はMQを監視します。消費者がメッセージを受信してビジネス処理が完了すると、MQにack(メッセージ確認)が送信されます。これは、消費者がメッセージとMQの通常の消費を完了したことを意味します。消費者にメッセージをプッシュしなくなります。そうしないと、消費者は引き続きメッセージを消費者に送信しようとします。ポイントサービスは「ポイントの追加」メッセージを受信し、ポイントの増加を開始します。ポイントが正常に増加すると、メッセージミドルウェアはackで応答します。それ以外の場合、メッセージミドルウェアはメッセージを繰り返し配信します。メッセージは繰り返し配信されるため、ポイントサービスの「ポイントの追加」機能は同一である必要があります。
第三に、ソリューション[RocketMQトランザクションメッセージプログラム ]
RocketMQは、Alibabaの分散メッセージングミドルウェアです。2012年にオープンソース化され、2017年に正式にトップのApacheプロジェクトになりました。アリババクラウドのメッセージング製品と買収した子会社を含むアリババグループのメッセージング製品の全ラインがRocketMQで実行されており、RocketMQは近年のダブルイレブンプロモーションで目を引くパフォーマンスを達成したと理解されています。Apache RocketMQバージョン4.3以降は、トランザクションメッセージを公式にサポートし、分散トランザクションの実装に便利なサポートを提供します。RocketMQトランザクションメッセージは、主にローカルトランザクション実行プロデューサー側でアトム問題メッセージを解決するために設計され、RocketMQはプロデューサー側との双方向通信機能ブローカーを設計 し、トランザクションコーディネーターとして生まれた ブローカーが存在するようにします;保存されたメカニズムRocketMQ自体が提供しますトランザクションメッセージの永続性機能を提供します。RocketMQの高可用性メカニズムと信頼性の高いメッセージ設計により、システムが異常な場合でもトランザクションメッセージがトランザクションの最終的な一貫性を確保できます。RocketMQ 4.3以降、完全なトランザクションメッセージが実装されます。これは、実際にはローカルメッセージテーブルのカプセル化です。ローカルメッセージテーブルはMQ内に移動され、プロデューサー側でのメッセージ送信とローカルトランザクション実行のアトミックな問題を解決します。
[実行プロセスは次のとおりです] :理解を容易にするために、登録の例を使用して、プロセス全体を説明するポイントを送信します。プロデューサーはMQの送信者であり、この場合はユーザーサービスであり、新しいユーザーの追加を担当します。MQサブスクライバーはメッセージコンシューマーです。この例では、MQサブスクライバーはポイントサービスであり、ポイントの追加を担当します。
[1]プロデューサーがトランザクションメッセージを送信します:プロデューサー(MQ送信者)がトランザクションメッセージをMQサーバーに送信し、MQサーバーがメッセージステータスを準備済み(準備済み状態)としてマークします。このメッセージコンシューマー(MQサブスクライバー)は現時点では消費できないことに注意してください到着した。この例では、プロデューサーは「ポイントの追加メッセージ」をMQサーバーに送信します。
[2] MQサーバー応答メッセージが正常に送信されました: MQサーバーは、プロデューサーから送信されたメッセージを受信し、正常に送信されて応答します。 MQがメッセージを受信したことを示します。
[3]プロデューサーはローカルトランザクションを実行します:プロデューサーはビジネスコードロジックを実行し、ローカルデータベーストランザクションによって制御されます。この例では、プロデューサーがユーザーの追加操作を実行します。
[4]は、メッセージの配信は:場合はプロデューサーのローカルトランザクションが正常に実行され、それがされます自動的に送信コミットメッセージを。MQSERVERに受信した後、 コミットメッセージを、MQサーバーは、ステータス「増加がメッセージをポイント」マークする消耗品、このとき、MQサブスクライバー(ポイントサービス)は通常どおりメッセージを消費します。プロデューサーのローカルトランザクションの実行が失敗した場合、プロデューサー は自動的にロールバックメッセージをMQサーバーに送信し、MQサーバーはロールバックメッセージを受信した後に「ポイントの増加メッセージ」を削除します。MQサブスクライバー(ポイントサービス)はメッセージを消費します。消費が成功した場合は、MQにackで応答します。それ以外の場合は、メッセージを繰り返し受信します。ここで、ackはデフォルトで自動的に応答します。つまり、プログラムが正常に実行されると、ackに自動的に応答します。
[5]トランザクションレビュー:プロデューサー側のローカルトランザクションの実行中に実行側がハングまたはタイムアウトした場合、MQサーバーは同じグループ内の他のプロデューサーに継続的にクエリを実行してトランザクション実行ステータスを取得します。このプロセスはトランザクションレビューと呼ばれます。MQサーバーは、トランザクションレビューの結果に基づいてメッセージを配信するかどうかを決定します。上記のメインプロセスはRocketMQによって実装されています。ユーザー側では、ユーザーはローカルトランザクション実行およびローカルトランザクションバックチェックメソッドを実装する必要があるため、ローカルトランザクションの実行ステータス(ローカルトランザクションステータステーブルを維持)のみで十分です。RoacketMQは、RocketMQLocalTransactionListenerインターフェイスを提供し ます。
public interface RocketMQLocalTransactionListener {
/**发送prepare消息成功此方法被回调,该方法用于执行本地事务
* @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
* @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
/**@param msg 通过获取transactionId来判断这条消息的本地事务执行状态
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
RocketMQLocalTransactionState checkLocalTransaction(Message msg);
}
[6]トランザクションメッセージの送信:以下は、トランザクションメッセージを送信するためにRocketMQによって提供されるAPIです。
TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//设置TransactionListener实现
producer.setTransactionListener(transactionListener);
//发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
第四に、RocketMQは、信頼できるメッセージの究極の一貫したトランザクションを実現します
[ビジネスの説明] RocketMQミドルウェアを介して、信頼できるメッセージの最終的な一貫した分散トランザクションを実現し、2つのアカウントの転送トランザクションプロセスをシミュレートします。2つのアカウントは異なる銀行(bank1のZhangSanとbank2のLiSi)にあり、bank1とbank2は2つのマイクロサービスです。トランザクションプロセスでは、ZhangSanが指定された金額をLiSiに転送します。上記のトランザクションステップ、Zhang Sanが金額を差し引き、bank2に転送メッセージを送信する場合、2つの操作は統合トランザクションである必要があります。
[プログラムコンポーネント]:
データベース:MySQL-5.7.25
2つのデータベースbank1とbank2を含みます。
JDK:64位jdk1.8.0_201
rocketmqサーバー:RocketMQ-4.5.0
rocketmqクライアント:RocketMQ-Spring-Boot-starter.2.0.2-RELEASE
マイクロサービスフレームワーク:spring-boot-2.1.3、spring-cloud-Greenwich.RELEASE
マイクロサービスとデータベースの関係:
dtx / dtx-txmsg-demo / dtx-txmsg-demo-bank1 Bank 1、Zhang Sanアカウントを操作し、データベースbank1に接続します
dtx / dtx-txmsg-demo / dtx-txmsg-demo-bank2 Bank 2、Li Siアカウントを操作し、データベースbank2に接続します
[コアコード] :プログラムの技術的構造は次のとおりです。
[対話プロセスは次のとおりです] :
[1]バンク1 MQサーバーに転送メッセージを送信します。
[2]バンク1は、ローカルトランザクションと実行
差し引く量を、[3]はバンク2のメッセージ、実行するローカルトランザクションを受け、金額を追加します。
[データベース] :追加de_duplicationバンク1内及びバンク2のデータベース、トランザクションレコードテーブル(重複排除テーブル)、トランザクションの独立した制御に使用されます。
DROP TABLE IF EXISTS `de_duplication`;
CREATE TABLE `de_duplication` (
`tx_no` varchar(64) COLLATE utf8_bin NOT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`tx_no`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
[バージョンの依存関係] : rocketmq-spring-boot-starterのバージョンが親プロジェクトで指定されています
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
[構成rocketMQ ] : application-local.propertisでrocketMQnameServerとプロダクショングループのアドレスを構成します。
rocketmq.producer.group = producer_bank2
rocketmq.name-server = 127.0.0.1:9876
[張さんサービスレイヤーコード] :
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Administrator
* @version 1.0
**/
@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {
@Autowired
AccountInfoDao accountInfoDao;
@Autowired
RocketMQTemplate rocketMQTemplate;
//向mq发送转账消息
@Override
public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
//将accountChangeEvent转成json
JSONObject jsonObject = new JSONObject();
jsonObject.put("accountChange", accountChangeEvent);
String jsonString = jsonObject.toJSONString();
//生成message类型
Message<String> message = MessageBuilder.withPayload(jsonString).build();
//发送一条事务消息
/**
* String txProducerGroup 生产组
* String destination topic,
* Message<?> message, 消息内容
* Object arg 参数
*/
rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1", "topic_txmsg", message, null);
}
//更新账户,扣减金额
@Override
@Transactional
public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
//幂等判断,txNo是在Ctroller中生成的 UUID,全局唯一
if (accountInfoDao.isExistTx(accountChangeEvent.getTxNo()) > 0) {
return;
}
//扣减金额
accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount() * -1);
//添加事务日志
accountInfoDao.addTx(accountChangeEvent.getTxNo());
if (accountChangeEvent.getAmount() == 3) {
throw new RuntimeException("人为制造异常");
}
}
}
[ Zhang San RocketMQLocalTransactionListener ] :ローカルトランザクションとトランザクションレビューを実行する2つのメソッドを実装するために、RocketMQLocalTransactionListenerインターフェイス実装クラスを記述します。
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Administrator
* @version 1.0
**/
@Component
@Slf4j
//生产者组与发送消息时定义组相同
@RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
public class ProducerTxmsgListener implements RocketMQLocalTransactionListener {
@Autowired
AccountInfoService accountInfoService;
@Autowired
AccountInfoDao accountInfoDao;
//事务消息发送后的回调方法,当消息发送给mq成功,此方法被回调
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try {
//解析message,转成AccountChangeEvent
String messageString = new String((byte[]) message.getPayload());
JSONObject jsonObject = JSONObject.parseObject(messageString);
String accountChangeString = jsonObject.getString("accountChange");
//将accountChange(json)转成AccountChangeEvent
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
//执行本地事务,扣减金额
accountInfoService.doUpdateAccountBalance(accountChangeEvent);
//当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//事务状态回查,查询是否扣减金额
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
//解析message,转成AccountChangeEvent
String messageString = new String((byte[]) message.getPayload());
JSONObject jsonObject = JSONObject.parseObject(messageString);
String accountChangeString = jsonObject.getString("accountChange");
//将accountChange(json)转成AccountChangeEvent
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
//事务id
String txNo = accountChangeEvent.getTxNo();
int existTx = accountInfoDao.isExistTx(txNo);
if (existTx > 0) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}
[ Li Siサービスレイヤーコード] :
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Administrator
* @version 1.0
**/
@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {
@Autowired
AccountInfoDao accountInfoDao;
//更新账户,增加金额
@Override
@Transactional
public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
log.info("bank2更新本地账号,账号:{},金额:{}", accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount());
if (accountInfoDao.isExistTx(accountChangeEvent.getTxNo()) > 0) {
return;
}
//增加金额
accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount());
//添加事务记录,用于幂等
accountInfoDao.addTx(accountChangeEvent.getTxNo());
if (accountChangeEvent.getAmount() == 4) {
throw new RuntimeException("人为制造异常");
}
}
}
[MQモニタークラス]: RocketMQListener インターフェイスを実装してターゲットトピックを監視します
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author Administrator
* @version 1.0
**/
@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2", topic = "topic_txmsg")
public class TxmsgConsumer implements RocketMQListener<String> {
@Autowired
AccountInfoService accountInfoService;
//接收消息
@Override
public void onMessage(String message) {
log.info("开始消费消息:{}", message);
//解析消息
JSONObject jsonObject = JSONObject.parseObject(message);
String accountChangeString = jsonObject.getString("accountChange");
//转成AccountChangeEvent
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
//设置账号为李四的
accountChangeEvent.setAccountNo("2");
//更新本地账户,增加金额
accountInfoService.addAccountInfoBalance(accountChangeEvent);
}
}
5、まとめ
信頼できるメッセージの究極の一貫性は、メッセージミドルウェアを介してプロデューサーからコンシューマーへのメッセージの一貫性を確保することです。この場合、RocketMQがメッセージミドルウェアとして使用されます。RocketMQは主に2つの機能を解決します。
[1]ローカルトランザクションとメッセージ送信原子の問題;
[2]トランザクション参加者が受信したメッセージの信頼性;
信頼できるメッセージの最終的な一貫性は、実行サイクルが長く、リアルタイム要件が低いシナリオに適しています。メッセージメカニズムの導入後、同期トランザクション操作はメッセージ実行に基づく非同期操作になり、分散トランザクションでの同期ブロック操作の影響を回避し、2つのサービスの分離を実現します。
●なぜアリババは90秒で100億に抵抗できるのですか?-サーバー側の高同時分散アーキテクチャの進化
● B2Beコマースプラットフォーム--ChinaPayUnionPay電子決済機能
● Zookeeperの分散ロックを学び、インタビュアーに感心してあなたを見てもらいましょう
● SpringCloudeコマーススパイクマイクロサービス-Redisson分散ロックソリューション
もっと良い記事をチェックして、公式アカウントを入力してください-私にお願いします-過去に素晴らしい
深くソウルフルなパブリックアカウント0.0