前言
MQ 相信程序员们都用过?但是MQ中的事务消息,估计用过的人不多。MQ的事务消息 在实现分布式事务中 可靠消息最终一致性这个方案中用得还是很多的🚀
还有就是,面试经常问到的消息丢失问题,使用事务消息当然也能解决这个问题咯👍
事务消息
了解事务消息,就得先了解,其常使用的场景。下面看看分布式事务基于MQ实现的两种方式吧。
分布式事务基于MQ的实现方式
在分布式事务场景中,实现的方案也比较多,比较常用的就是基于MQ来实现。在MQ的实现的方案中,常用两个案有以下两种
- 最大努力通知:
他不需要保证消费者一定能接收到,只是尽自己最大的努力去通知就行了。最多就是在发消息的地方加一个重试的机制,这个方案可能会导致消息的重复和丢失 ,不适合用在一些一致性要求较高的场景,用来发通知等还可以。涉及金额的谨慎使用这种方式了。 - 可靠消息最终一致性:
依赖可靠的消息 ,来实现一种最终一致性的模型。 普通的消息发送,可能会因为网络原因,超时或者失败,或者成功但是没有收到响应。 (事务消息可以保证MQ一定可以发送成功,或者是如果失败了,也有机制能够重试让他成功,这就是实现了消息的可靠性。)
实现可靠消息 除了 事务消息 这种方式,还有一种就是本地消息,在提交业务事务中,将消息也进行写入到数据库,利用定时任务去扫描 数据库中 消息的状态,进行消息的重试或者消息状态的更正。
所以事务消息 用来保证消息的可靠性的一种方式,从而实现可靠消息最终一致性的分布式事务。
可靠消息最终一致性
利用 事务消息 和 本地事务,实现事务的最终一致性。实现的主要步骤如下
1、客户端A执行本地事务
2、客户端A向客户端B发送MQ消息
3、客户端B收到MQ消息后执行自己的本地事务
RocketMQ实现事务消息
实现原理
RocketMQ 的事务消息实现基于两阶段提交思想,主要分为三个阶段:
-
发送半消息:生产者先向消息队列发送半消息(Half Message),半消息对于消费者是不可见的,只有在后续被确认提交后才会对消费者可见。
-
执行本地事务:发送半消息成功后,生产者执行本地事务,根据本地事务的执行结果决定是提交还是回滚半消息。
-
消息状态确认:
- 如果本地事务执行成功,生产者向消息队列发送提交请求,将半消息变为可消费消息。
- 如果本地事务执行失败,生产者向消息队列发送回滚请求,消息队列会删除该半消息。
- 如果生产者没有及时发送提交或回滚请求,消息队列会主动回查生产者,询问该半消息对应的本地事务状态。
代码示例
1.引入依赖
xml
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.2</version>
</dependency>
- 创建生产者
java
import org.apache.rocketmq.client.exception.MQClientException;
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.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.concurrent.*;
public class TransactionalProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 创建事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_group");
// 设置 NameServer 地址
producer.setNamesrvAddr("localhost:9876");
// 创建线程池用于执行本地事务
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
});
// 设置线程池
producer.setExecutorService(executorService);
// 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行本地事务
try {
System.out.println("Executing local transaction...");
// 模拟本地事务执行
Thread.sleep(1000);
// 假设本地事务执行成功
return LocalTransactionState.COMMIT_MESSAGE;
} catch (InterruptedException e) {
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 消息队列回查本地事务状态
System.out.println("Checking local transaction state...");
// 假设本地事务执行成功
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 启动生产者
producer.start();
// 发送半消息
try {
Message msg = new Message("TransactionTopic", "TransactionTag", "Hello, RocketMQ Transaction Message".getBytes());
producer.sendMessageInTransaction(msg, null);
} catch (MQClientException e) {
e.printStackTrace();
}
// 等待一段时间后关闭生产者
Thread.sleep(5000);
producer.shutdown();
}
}
3.创建消费者
java
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class TransactionalConsumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
// 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction_consumer_group");
// 设置 NameServer 地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅主题
consumer.subscribe("TransactionTopic", "*");
// 设置消息监听器
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
for (MessageExt msg : msgs) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
总结
本篇文章,介绍了什么是事务消息,以及事务消息的运用场景和使用案例。如果觉得有用欢迎一键三连👍👍👍
RocketMQ 官网也对 事务消息 有详细的说明,并且还有 java 案例,非常值得阅读的 :rocketmq.apache.org/zh/docs/fea...