CAP 定理(也称为 Brewer 定理),指的是在分布式系统环境下,有3个核心的需求:
分布式系统不可能同时满足上面三种,最多同时满足其中两种,也就是CA、CP、AP
顾名思义,事务消息主要用于分布式应用中,解决分布式事务的问题,且是采用的最终一致的方案。
此处我们采用官网案例:用户支付订单操作,此业务操作的处理分支包括:
此操作涉及这么多的下游系统,如果采用强一致性事务来实现,首先会导致事务控制时间太长,事务控制的范围太大,进一步导致系统并发效率低下,系统性能也低。
此图我们还是按照用户订单支付操作的例子来说明,订单支付操作是订单系统的操作,其对应一个本地事务(branch 2),其余下游系统都是在这个支付事务之后需要执行的事务(branch 2.1、branch 2.2、branch 2.3)。
RocketMQ 事务消息,保证的是 branch 2 的事务如果成功,则 MQ 服务端就一定会有一条对应的半事务消息。如果 branch 2 的事务回滚,则 MQ 服务端也会回滚对应的半事务消息,此需要生产者来保证。
branch 2.1、branch 2.2、branch 2.3 是由各个子系统中对应的事务消息的消费者来实现的,只要 branch 2 成功,那么对应的 2.1 、2.2 、2.3 都必须要执行成功,此需要消费者来保证。
图中黄色线条为特殊情况下的状态回查流程。
生产者将消息发送至Apache RocketMQ服务端。
Apache RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
生产者开始执行本地事务逻辑。
生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 说明 服务端回查的间隔时间和最大回查次数,请参见参数限制。
生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
$> ./mqadmin updatetopic -n localhost:9876 -c DefaultCluster -t MY_TRANSACTION_TOPIC -a +message.type=TRANSACTION
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.commons.collections.MapUtils;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.Transaction;
import org.apache.rocketmq.client.apis.producer.TransactionResolution;
import java.util.Map;
public class TransactionProducerDemo {
public static void main(String[] args) {
// 用于提供:生产者、消费者、消息对应的构建类 Builder
ClientServiceProvider provider = ClientServiceProvider.loadService();
// 构建配置类(包含端点位置、认证以及连接超时等的配置)
ClientConfiguration configuration = ClientConfiguration.newBuilder()
// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
.setEndpoints(MyMQProperties.ENDPOINTS)
.build();
// 构建生产者
Producer producer = null;
try {
producer = provider.newProducerBuilder()
// Topics 列表:生产者和主题是多对多的关系,同一个生产者可以向多个主题发送消息
.setTopics("MY_TRANSACTION_TOPIC")
.setClientConfiguration(configuration)
// 设置回查对象 TransactionChecker(注意:此方法回查的是订单系统本地事务是否成功,以决定当前消息事务是否提交或回滚)
.setTransactionChecker(messageView -> {
Map<String,String> p = messageView.getProperties();
if(MapUtils.isEmpty(p)){
// 说明回查的消息有误,直接回滚(此处是回滚的消息事务,半事务消息将不会投递)
return TransactionResolution.ROLLBACK;
}
String orderId = p.get("orderId");
String status = p.get("status");
// 验证订单系统本地数据库事务是否成功
return checkOrderStatus(orderId,status) ? TransactionResolution.COMMIT : TransactionResolution.ROLLBACK;
})
// 构建生产者,此方法会抛出 ClientException 异常
.build();
} catch (ClientException e) {
throw new RuntimeException(e);
}
// 开启消息事务
final Transaction transaction = beginTransaction(producer);
// 定义消息
Message message = provider.newMessageBuilder()
// 设置消息发送到的主题
.setTopic("MY_TRANSACTION_TOPIC")
// 设置消息索引键,可根据关键字精确查找某条消息。其一般为业务上的唯一值。如:订单id
.setKeys("order_id_10")
// 设置消息Tag,当前为订单支付
.setTag("ORDER_PAY")
// 添加回查校验需要的信息
.addProperty("order_id","10").addProperty("status","paid")
// 消息体,单条消息的传输负载不宜过大。所以此处的字节大小最好有个限制
.setBody(("{\"success\":true,\"msg\": 事务消息发送成功}").getBytes())
.build();
try {
// 发送半事务消息(此处不要使用异步发送,因为执行的顺序即为半事务消息发送后执行本地事务逻辑)
producer.send(message,transaction);
// 执行本地数据库事务
doLocalTransaction();
// 本地事务执行成功,提交发送消息事务
commitTransaction(transaction);
} catch (ClientException e) {
e.printStackTrace();
// 半事务消息发送失败或者本地数据库事务执行失败,都回滚消息事务
rollbackTransaction(transaction);
throw new RuntimeException(e);
} catch (Exception e){
e.printStackTrace();
rollbackTransaction(transaction);
throw new RuntimeException(e);
}
}
/**
* 验证订单系统当前的订单状态
* @param orderId 订单id
* @param status 当前对应的状态
* @return
*/
public static final boolean checkOrderStatus(String orderId,String status){
// 通过 sql 或代码进行业务状态验证,检查订单系统本地数据库事务是否成功
// 比如数据库信息如下:
String dbOrderId = "10";
String dbStatus = "paid";
if(dbOrderId.equals(orderId) && dbStatus.equals(status)){
return true;
}
return false;
}
public static final void rollbackTransaction(Transaction transaction){
try {
transaction.rollback();
} catch (ClientException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static final void commitTransaction(Transaction transaction){
try {
transaction.commit();
} catch (ClientException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static final Transaction beginTransaction(Producer producer){
try {
return producer.beginTransaction();
} catch (ClientException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 本地事务方法
* 实际应用中,此方法应该是定义在 Service 中
* 且进行本地事务控制,一般情况下出现异常回滚事务,正常情况提交事务
*/
public static final void doLocalTransaction(){
// 本地订单系统业务相关操作代码
// throw new RuntimeException("本地事务失败");
}
}
如果 doLocalTransaction 发生异常,则半事务消息会回滚。
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
import java.nio.ByteBuffer;
import java.util.Collections;
public class TranscationConsumerDemo {
public static void main(String[] args) throws ClientException {
// 用于提供:生产者、消费者、消息对应的构建类 Builder
ClientServiceProvider provider = ClientServiceProvider.loadService();
// 构建配置类(包含端点位置、认证以及连接超时等的配置)
ClientConfiguration configuration = ClientConfiguration.newBuilder()
// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
.setEndpoints(MyMQProperties.ENDPOINTS)
.build();
// 设置过滤条件(这里为使用 tag 进行过滤)
String tag = "ORDER_PAY";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 构建消费者
PushConsumer pushConsumer = provider.newPushConsumerBuilder()
.setClientConfiguration(configuration)
// 设置消费者分组
.setConsumerGroup("MY_TRANSACTION_GROUP")
// 设置主题与消费者之间的订阅关系
.setSubscriptionExpressions(Collections.singletonMap("MY_TRANSACTION_TOPIC", filterExpression))
.setMessageListener(messageView -> {
System.out.println(messageView);
System.out.println(messageView.getProperties());
ByteBuffer rs = messageView.getBody();
byte[] rsByte = new byte[rs.limit()];
rs.get(rsByte);
System.out.println("Message body:" + new String(rsByte));
// 处理消息并返回消费结果。
System.out.println("Consume message successfully, messageId=" + messageView.getMessageId());
return ConsumeResult.SUCCESS;
}).build();
// 如果不需要再使用 PushConsumer,可关闭该实例。
// pushConsumer.close();
}
}
我们的消费者代码和普通消息的消费者是一样的,无需特殊的处理。只不过,我们的下游有多个子系统,就需要多个消费者,生产者和消费者之间是一对多的订阅关系,我们在入门介绍一章中已经介绍,为消费者定义不同的消费分组即可。
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
import java.nio.ByteBuffer;
import java.util.Collections;
public class TranscationConsumerDemo {
public static void main(String[] args) throws ClientException {
// 用于提供:生产者、消费者、消息对应的构建类 Builder
ClientServiceProvider provider = ClientServiceProvider.loadService();
// 构建配置类(包含端点位置、认证以及连接超时等的配置)
ClientConfiguration configuration = ClientConfiguration.newBuilder()
// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
.setEndpoints(MyMQProperties.ENDPOINTS)
.build();
// 设置过滤条件(这里为使用 tag 进行过滤)
String tag = "ORDER_PAY";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 模拟物流子系统消费者
provider.newPushConsumerBuilder()
.setClientConfiguration(configuration)
// 设置消费者分组
.setConsumerGroup("MY_TRANSACTION_WMS_GROUP")
// 设置主题与消费者之间的订阅关系
.setSubscriptionExpressions(Collections.singletonMap("MY_TRANSACTION_TOPIC", filterExpression))
.setMessageListener(messageView -> {
System.out.println(messageView);
System.out.println(messageView.getProperties());
ByteBuffer rs = messageView.getBody();
byte[] rsByte = new byte[rs.limit()];
rs.get(rsByte);
System.out.println("物流子系统:Message body:" + new String(rsByte));
// 处理消息并返回消费结果。
System.out.println("物流子系统:Consume message successfully, messageId=" + messageView.getMessageId());
return ConsumeResult.SUCCESS;
}).build();
// 模拟积分子系统消费者
provider.newPushConsumerBuilder()
.setClientConfiguration(configuration)
// 设置消费者分组
.setConsumerGroup("MY_TRANSACTION_UPS_GROUP")
// 设置主题与消费者之间的订阅关系
.setSubscriptionExpressions(Collections.singletonMap("MY_TRANSACTION_TOPIC", filterExpression))
.setMessageListener(messageView -> {
System.out.println(messageView);
System.out.println(messageView.getProperties());
ByteBuffer rs = messageView.getBody();
byte[] rsByte = new byte[rs.limit()];
rs.get(rsByte);
System.out.println("积分子系统:Message body:" + new String(rsByte));
// 处理消息并返回消费结果。
System.out.println("积分子系统:Consume message successfully, messageId=" + messageView.getMessageId());
return ConsumeResult.SUCCESS;
}).build();
// 如果不需要再使用 PushConsumer,可关闭该实例。
// pushConsumer.close();
}
}
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.commons.lang3.StringUtils;
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.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TransactionProducerDemo {
/**
* 生产者分组
*/
private static final String PRODUCER_GROUP = "TRANSCATION_PRODUCT_GROUP";
/**
* 主题
*/
private static final String TOPIC = "MY_TRANSCATION_TOPIC";
public static void main(String[] args) {
// 注意:事务消息使用 TransactionMQProducer
TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP);
// 设置 事务回查的线程池
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(){
/**
* 半事务消息发送成功后,执行本地事务的方法
* @param msg 半事务消息
* @param arg 执行本地事务需要的业务参数
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 模拟本地数据库事务执行
try {
System.out.println("执行本地事务:" + msg);
System.out.println("执行本地事务:" + arg);
doLocalTransaction();
}catch (Exception e){
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
/**
* MQ 服务端未收到消息提交或回滚的确认,二次检查本地事务是否执行成功的方法
* @param msg 要回查的消息
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String orderId = msg.getProperty("orderId");
String status = msg.getProperty("status");
if(StringUtils.isEmpty(orderId) || StringUtils.isEmpty(status)){
// 说明回查的消息有误,直接回滚(此处是回滚的消息事务,半事务消息将不会投递)
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 通过 sql 或代码进行业务状态验证,检查订单系统本地数据库事务是否成功
// 比如数据库信息如下:
String dbOrderId = "10";
String dbStatus = "paid";
if(dbOrderId.equals(orderId) &&
dbStatus.equals(status)){
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
});
/*
* NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876
*/
producer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);
/*
* 发送消息超时时间,默认即为 3000
*/
producer.setSendMsgTimeout(3000);
try {
producer.start();
} catch (MQClientException e) {
throw new RuntimeException(e);
}
// 发送事务消息
Message msg = new Message();
msg.setTopic(TOPIC);
// 设置消息索引键,可根据关键字精确查找某条消息。
msg.setKeys("order_id_10");
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
msg.setTags("ORDER_PAY");
// 设置消息体
msg.setBody(("{\"success\":true,\"msg\": Remoting 协议事务消息发送成功}").getBytes());
// 添加 Properties
msg.putUserProperty("orderId","10");
msg.putUserProperty("status","paid");
try {
// 发送事务消息
producer.sendMessageInTransaction(msg,"业务参数对象");
} catch (MQClientException e) {
throw new RuntimeException(e);
}
}
/**
* 本地事务方法
* 实际应用中,此方法应该是定义在 Service 中
* 且进行本地事务控制,一般情况下出现异常回滚事务,正常情况提交事务
*/
public static final void doLocalTransaction(){
// 本地订单系统业务相关操作代码
// throw new RuntimeException("本地事务失败");
}
}
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
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.consumer.ConsumeFromWhere;
public class TransactionConsumerDemo {
/**
* 设置消费者分组
*/
public static final String CONSUMER_GROUP = "TRANSCATION_CONSUMER_GROUP";
/**
* 主题
*/
public static final String TOPIC = "MY_TRANSCATION_TOPIC";
public static void main(String[] args) throws MQClientException {
/*
* 通过消费者分组,创建消费者
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
/*
* NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876
*/
consumer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);
/*
* 指定从哪一个消费位点开始消费 CONSUME_FROM_FIRST_OFFSET 表示从第一个开始
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
/*
* 消费者订阅的主题,和过滤条件
* 我们这里使用 * 表示,消费者消费主题下的所有消息,多个tag 使用 || 隔开
*/
consumer.subscribe(TOPIC, "ORDER_PAY");
/*
* 注册消费监听
*/
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
/*
* 启动消费者.
*/
consumer.start();
System.out.printf("Consumer Started.%n");
// 如果消费者不再使用,关闭
// consumer.shutdown();
}
}