一:背景
对于消息队列,必定绕不开的几个问题是:消息的可靠性,消息的重复,消息的顺序,消息的事务性。前面已经讲了消息的顺序性问题,接下来要了解的就是消息的事务性问题了。好,接下来就来了解一下消息的事务。
二:实例
producer(生产者):
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
String group_name = "transaction_producer";
final TransactionMQProducer producer = new TransactionMQProducer(group_name);
producer.setNamesrvAddr("192.168.0.2:9876;192.168.0.3:9876");
producer.setCheckRequestHoldMax(200);
producer.setCheckThreadPoolMaxSize(20);
producer.setCheckThreadPoolMinSize(5);
producer.start();
//服务器回调producer,检查本地事务分支成功还是失败
producer.setTransactionCheckListener(new TransactionCheckListener() {
@Override
public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
System.out.println("state --" + new String(msg.getBody()));
return LocalTransactionState.COMMIT_MESSAGE;
}
});
TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
for(int i=0; i<2 ; i++) {
try {
Message msg = new Message("TopicTransaction","transaction" + i,"key",("hello " + i).getBytes());
SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, "tq");
System.out.println(sendResult);
}catch(Exception e) {
e.printStackTrace();
}
}
Thread.sleep(3000);
producer.shutdown();
}
}
TransactionExecuterImpl:
/*
* 执行本地事务,由客户端回调
*/
public class TransactionExecuterImpl implements LocalTransactionExecuter {
@Override
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
System.out.println("msg :" + new String(msg.getBody()));
System.out.println("arg :" + arg);
String tag = msg.getTags();
if(tag.equals("transaction1")) {
//这里有一个分阶段提交任务概念
System.out.println("这里处理业务逻辑,如操作数据库,失败情况下进行ROLLBACK");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
//return LocalTransactionState.UNKNOW;
}
}
consumer(消费者):
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction_consumer");
//设置consumer第一次启动是从队列头部开始还是尾部开始消费,若非第一次启动,那么按照上次消费的位置继续消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicTransaction", "*");
//批量消费,一次消费多少条消息,默认为1条,最大情况能拿多少条不代表每次能拿这么多条
//consumer.setConsumeMessageBatchMaxSize(3);
//messageListenerOrderly 保证顺序消费,消费端接收的是同一个队列的消息,避免多线程时顺序错乱
/*consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> arg0, ConsumeOrderlyContext arg1) {
return null;
}
});*/
consumer.setConsumeThreadMax(10);
consumer.setConsumeThreadMin(10);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
//System.out.println(Thread.currentThread().getName() + "Receive: " + msgs);
//获取一次性消费多少条消息
//System.out.println("消息条数 : " + msgs.size());
MessageExt msg1 = null;
try {
for(MessageExt msg : msgs) {
msg1 = msg;
String topic = msg.getTopic();
String msgbody = new String(msg.getBody(),"utf-8");
String tag = msg.getTags();
System.out.println("收到消息: " + "topic:" + topic + " tags:" + tag + " msg:" + msgbody);
}
} catch (Exception e) {
e.printStackTrace();
//若已经重试了5次则不再重试
if(msg1.getReconsumeTimes() == 5) {
//此处记录日志操作。。。
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.setNamesrvAddr("192.168.0.2:9876;192.168.0.3:9876");
consumer.start();
System.out.println("Consumer started...");
}
}
结果输出:
producer console:
msg :hello 0
arg :tq
SendResult [sendStatus=SEND_OK, msgId=C0A8000200002A9F0000000000003B1C, messageQueue=MessageQueue [topic=TopicTransaction, brokerName=broker-a, queueId=0], queueOffset=0]
msg :hello 1
arg :tq
这里处理业务逻辑,如操作数据库,失败情况下进行ROLLBACK
SendResult [sendStatus=SEND_OK, msgId=C0A8000200002A9F0000000000003C9E, messageQueue=MessageQueue [topic=TopicTransaction, brokerName=broker-a, queueId=1], queueOffset=0]
consume console:
收到消息: topic:TopicTransaction tags:transaction0 msg:hello 0
三:原理
1.0、 Producer发送消息到RocketMQ,这条消息我们暂且称之为message1 并且是transaction状态的事务消息。
1.1、MQ把message1存入数据库,并且是状态是prepared。
1.2、RocketMQ回调Producer中的本地事务。(本地事务由三个状态COMMIT_MESSAGE、ROLLBACK_MESSAGE、UNKNOW)。
1.2.1 、本地事务处理完成后,无论成功还是失败都会有一个状态,如果成功的话,Producer就会发送COMMIT_MESSAGE状态表示确认消息到RocketMQ上。
1.2.2、然后把message1这个消息存储到consumer queue中,并在数据库中把这条prepared的消息标记为commited。
1.2.3、这条消息就被Consumer消费了。
1.3.1、如果Producer的事务处理返回了一个UNKNOW状态。因为broker会定时的去扫描数据库,如果数据库中的数据状态是commited的,那么就清除这条数据。
1.4、如果数据库中数据的状态还是prepared的。那MQ就会主动的去调用Producer中的check方法。
1.5.1、check方法再去查本地的数据库看有没有减钱,如果没减钱的话就rollback,
1.6.1、rollback后Producer又发了一条ROLLBACK_MESSAGE给MQ。
1.7.1、MQ收到这条消息后,就会把MQ的数据库中对应的prepared数据给清除掉。那么这条数据也就不会被Consumer端消费了
1.5.2、check方法查本地数据库看有没有减钱,如果减钱了。
1.6.2、会给MQ发送一个COMMIT_MESSAGE。
1.7.2、MQ还会去查自己的数据库,然后把数据库中对应的数据给清除掉
四:致谢
本文主要摘抄自以下文章: