1. RocketMQトランザクションメッセージの原則:
RocketMQは、バージョン4.3以降に完全なトランザクションメッセージを実装しました。MQベースの分散トランザクションスキームは、基本的にローカルメッセージテーブルのカプセル化です。全体的なプロセスはローカルメッセージテーブルと一貫しています。唯一の違いは、ローカルメッセージテーブルが保存されることです。トランザクションメッセージは、ビジネスデータベースではなく内部的に、本番側でのメッセージ送信とローカルトランザクションの実行というアトミックな問題を解決します。MQ本番側が複数の送信を行わずにメッセージを正しく送信するには、ここでの境界を明確にする必要があります。送信後に消費者が通常の消費メッセージを持っているかどうかに関しては、この異常なシナリオはMQメッセージ消費失敗再試行メカニズムによって保証されます。
RocketMQ設計におけるブローカーとプロデューサー間の双方向通信機能により、ブローカーは本質的にトランザクションコーディネーターになります。RocketMQ自体が提供するストレージメカニズムは、トランザクションメッセージの永続性機能を提供します。RocketMQの高可用性メカニズムと信頼性の高いメッセージ設計は、トランザクションメッセージです。システムで例外が発生した場合でも、トランザクションの結果整合性を確保できます。
1.トランザクションの一貫性を実現するためのRocketMQの原則:
注:ローカルトランザクションのロールバックは、ローカルDBのACID機能に依存し、サブスクライバーの正常な消費は、MQサーバーの失敗再試行メカニズムによって保証されます。
(1)通常の状況:トランザクションのアクティブなサービスが正常で障害がない場合、メッセージ送信プロセスは次のようになります。
ステップ1:MQ送信者が半分のメッセージをMQサーバーに送信し、MQサーバーがメッセージステータスを準備完了としてマークします。この時点で、MQサブスクライバーはメッセージを消費できません。
ステップ2:MQサーバーがメッセージを正常に永続化した後、メッセージが正常に受信されたことを送信者ACKに確認します
ステップ3:送信者はローカルトランザクションロジックの実行を開始します
ステップ4:送信者は、ローカルトランザクションの実行結果に応じて、二次確認、コミット、またはロールバックをMQサーバーに送信します
最終ステップ:MQサーバーがコミット操作を受信すると、半分のメッセージを配信可能としてマークし、MQサブスクライバーは最終的にメッセージを受信します。ロールバック操作を受信すると、半分のメッセージを削除し、サブスクライバーはメッセージを受け入れない;ローカルトランザクションの実行結果が応答しないかタイムアウトした場合、MQサーバーはトランザクションステータスをチェックします。詳細については、手順(2)の例外の説明を参照してください。
(2)異常な状況:異常なネットワーク切断またはアプリケーションの再起動の場合、図のステップ4で送信された2番目の確認タイムアウトがMQサーバーに到達せず、このときの処理ロジックは次のとおりです。
ステップ⑤:MQサーバーはメッセージをチェックします
ステップ✧:送信者がメッセージチェックバックを受信した後、メッセージのローカルトランザクション実行結果を確認します
ステップ✧:送信者は、チェックによって取得されたローカルトランザクションの最終状態に従って、2番目の確認を再度送信します。
最終ステップ:MQサーバーはコミット/ロールバックに基づいてメッセージを配信または削除します
2. RocketMQトランザクションメッセージの実装プロセス:
RocketMQ 4.5.2を例にとると、トランザクションメッセージ専用のキューRMQ_SYS_TRANS_HALF_TOPICがあります。すべての準備メッセージが最初にここに配置されます。メッセージがコミット要求を受信すると、メッセージはコンシューマーによる消費のために実際のトピックキューに転送されます。 RMQ_SYS_TRANS_OP_HALF_TOPICにメッセージを詰めている間。簡単なフローチャートは次のとおりです。
中断やその他のネットワーク上の理由でアプリケーションモジュールのトランザクションにすぐに応答できない場合、RocketMQはそれをUNKNOWとして扱います。このRocketMQトランザクションメッセージは、トランザクションメッセージのトランザクション実行ステータスを定期的に確認するという解決策を提供します。簡単なフローチャートは次のとおりです。次のように:
2. SpringbootはRocketMQを統合して、トランザクションメッセージを実装します。
このセクションでは、SpringBootがRocketMQを統合し、トランザクションメッセージを使用して、「注文して在庫を差し引く」場合の結果整合性を確保する方法を紹介します。コアアイデアは、注文サービス(生産側)が在庫控除メッセージをRocketMQに送信し、次にローカル注文生成ロジックを実行し、最後にそれをRocketMQに送信して、在庫を控除するように在庫サービスに通知し、在庫控除メッセージが通常消費されます。
この場合に使用されるサービスは、注文サービスと在庫サービスの2つに分けられます。関連する主要なデータベーステーブルは、注文テーブル、ストレージテーブル、ローカルトランザクションステータステーブルの3つです。これらのテーブルは比較的単純であるため、対応するテーブル構築ステートメントはここに貼り付けられず、対応するPojoオブジェクト、Daoレイヤー、およびサービスレイヤーのコードも貼り付けられません。以下はコアロジックのコードのみを示しています。
1.RocketMQサーバーを起動します。
RocketMQのインストールと展開については、次の記事を参照してください:https ://blog.csdn.net/a745233700/article/details/122531859
2.親pomファイルに依存関係を導入します。
<!-- rocketmq 事务消息 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
3.製品コード:
本番側のコアロジックは、トランザクションメッセージをRocketMQに配信し、ローカルトランザクションを実行し、最後にローカルトランザクションの実行結果をRocketMQに通知することです。
(1)RocketMQ関連の構成:
次の構成をapplication.properties構成ファイルに追加します。
rocketmq.name-server=172.28.190.101:9876
rocketmq.producer.group=order_shop
(2)リスナークラスを作成します。
TransactionListenerインターフェースを実装し、実装されたデータベーストランザクションコミットメソッドexecuteLocalTransaction()およびトランザクションステータスチェックバックメソッドcheckLocalTransaction()で結果をシミュレートします。
/**
* rocketmq 事务消息回调类
*/
@Slf4j
@Component
public class OrderTransactionListener implements TransactionListener
{
@Resource
private ShopOrderMapper shopOrderMapper;
/**
* half消息发送成功后回调此方法,执行本地事务
*
* @param message 回传的消息,利用transactionId即可获取到该消息的唯一Id
* @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
@Override
@Transactional
public LocalTransactionState executeLocalTransaction(Message message, Object arg)
{
log.info("开始执行本地事务:订单信息:" + new String(message.getBody()));
String msgKey = new String(message.getBody());
ShopOrderPojo shopOrder = JSONObject.parseObject(msgKey, ShopOrderPojo.class);
int saveResult;
LocalTransactionState state;
try
{
//修改为true时,模拟本地事务异常
boolean imitateException = true;
if(imitateException)
{
throw new RuntimeException("更新本地事务时抛出异常");
}
// 生成订单,本地事务的回滚依赖于DB的ACID特性,所以需要添加Transactional注解。当本地事务提交失败时,返回ROLLBACK_MESSAGE,则会回滚rocketMQ中的half message,保证分布式事务的一致性。
saveResult = shopOrderMapper.insert(shopOrder);
state = saveResult == 1 ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
// 更新本地事务并将事务号持久化,为后续的幂等做准备
// TransactionDao.add(transactionId)
}
catch (Exception e)
{
log.error("本地事务执行异常,异常信息:", e);
state = LocalTransactionState.ROLLBACK_MESSAGE;
}
//修改为true时,模拟本地事务超时,对于超时的消息,rocketmq会调用checkLocalTransaction方法回查本地事务执行状况
boolean imitateTimeout = false;
if(imitateTimeout)
{
state = LocalTransactionState.UNKNOW;
}
log.info("本地事务执行结果:msgKey=" + msgKey + ",execute state:" + state);
return state;
}
/**
* 回查本地事务接口
*
* @param messageExt 通过获取transactionId来判断这条消息的本地事务执行状态
* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt)
{
log.info("调用回查本地事务接口:msgKey=" + new String(messageExt.getBody()));
String msgKey = new String(messageExt.getBody());
ShopOrderPojo shopOrder = JSONObject.parseObject(msgKey, ShopOrderPojo.class);
// 备注:此处应使用唯一ID查询本地事务是否执行成功,唯一ID可以使用事务的transactionId。但为了验证方便,只查询DB的订单表是否存在对应的记录
// TransactionDao.isExistTx(transactionId)
List<ShopOrderPojo> list = shopOrderMapper.selectList(new QueryWrapper<ShopOrderPojo>()
.eq("shop_id", shopOrder.getShopId())
.eq("user_id", shopOrder.getUserId()));
LocalTransactionState state = list.size() > 0 ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
log.info("调用回查本地事务接口的执行结果:" + state);
return state;
}
}
検証の便宜のために、上記のデモでは2つのブール変数limitateExceptionとlimitTimeoutを使用して、それぞれトランザクション実行の例外とタイムアウトをシミュレートしています。ブール値をtrueに設定するだけで済みます。
(3)配信トランザクションメッセージ:
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrderPojo> implements ShopOrderService
{
@Resource
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderTransactionListener orderTransactionListener;
/**
* 发送事务消息
*/
@Override
public boolean sendOrderRocketMqMsg(ShopOrderPojo shopOrderPojo)
{
String topic = "storage";
String tag = "reduce";
// 设置监听器,此处如果使用MQ其他版本,可能导致强转异常
((TransactionMQProducer) rocketMQTemplate.getProducer()).setTransactionListener(orderTransactionListener);
//构建消息体
String msg = JSONObject.toJSONString(shopOrderPojo);
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(msg).build();
//发送事务消息,由消费者进行进行减少库存
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(topic + ":" + tag , message, null);
log.info("Send transaction msg result: " + sendResult);
return sendResult.getSendStatus() == SendStatus.SEND_OK;
}
}
4.消費者コード:
コンシューマーのコアロジックは、MQをリッスンしてメッセージを受信することです。メッセージを受信した後、在庫を差し引きます。
(1)RocketMQ関連の構成:
次の構成をapplication.properties構成ファイルに追加します。
rocketmq.name-server=172.28.190.101:9876
rocketmq.consumer.group=order_shop
(2)消費者監視クラス:
import com.alibaba.fastjson.JSONObject;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 库存管理消费者类
**/
@Component
@RocketMQMessageListener (consumerGroup = "order_storage", topic = "storage")
public class StorageConsumerListener implements RocketMQListener<String>
{
@Resource
private TStorageService tStorageService;
/**
* rocketMQ消费者
*/
@Override
public void onMessage(String message)
{
System.out.println("消费者开始消费:从MQ中获取的消息是:" + message);
ShopOrderPojo shopOrder = JSONObject.parseObject(message, ShopOrderPojo.class);
// 1、幂等校验,防止消息重复消费--此处省略相关的代码逻辑:
// TransactionDao.isExistTx(transactionId)
// 2、执行消息消费操作--减少商品库存:
TStoragePojo shop = tStorageService.getById(shopOrder.getShopId());
shop.setNum(shop.getNum() - 1);
boolean updateResult = tStorageService.updateById(shop);
// 3、添加事务操作记录--此次省略代码:
// TransactionDao.add(transactionId)
System.out.println("消费者完成消费:操作结果:" + updateResult);
}
}
この時点で、RocketMQトランザクションメッセージに基づく完全な分散トランザクションの結果整合性が完了します。