事务消息收发实战
场景介绍
事务是一组操作组成,一个原子操作要么都成功,要么都失败。
如果任务是在本地,比如操作数据库,可以把一组操作放到事务里面。但是发送消息是网络远程操作,怎么保证事务呢?
正常的思路可能是先要用一张表来记录每一步骤的状态,然后监听这个状态来处理。举个常见的例子比如转账业务,我们发送一条消息到MQ,这条消息是远程累加B的余额,在累加B的余额之前我们要扣除A的余额扣除成功后这条消息才能到消费方执行真正的逻辑增加B的余额,这两个操作是一个事务操作。那么RocketMq提供了怎样的能力实现这个场景呢,看下面这个图:
代码实操
生产者
public class Producer {
public static void main(String[] args) throws Exception {
TransactionMQProducer producer = new TransactionMQProducer("producer_group_trans");
producer.setNamesrvAddr("ip:9876");
producer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 这里需要写真正的业务逻辑
// 如果业务执行成功就返回成功,提交事务
// 如果业务执行失败就返回失败,回滚事务
System.out.println("============================ 检查事务状态 ===========================");
try {
boolean state = getState();
if (state) {
System.out.println("================= 成功提交了消息:" + new String(msg.getBody()));
return LocalTransactionState.COMMIT_MESSAGE;
}
System.out.println("================= 回滚消息:" + new String(msg.getBody()));
return LocalTransactionState.ROLLBACK_MESSAGE;
} catch (Exception e) {
e.printStackTrace();
System.out.println("================= 发生了异常,要进行异常检查 " + new String(msg.getBody()));
// 发生异常这里状态这是为UNKNOW 过一段时间就会触发checkLocalTransaction去尽显再次确认
return LocalTransactionState.UNKNOW;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("============================ 异常检查机制 ===========================");
// 当发生异常了这里来判断到底业务到底是否执行成功
String property = msg.getProperty("state");
if ("true".equals(property)) {
System.out.println("================= 异常检查通过,成功发送消息:" + new String(msg.getBody()));
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
});
producer.start();
// 生成3大类消息
List<Order> F = OrderBuilder.build(1, 4, "TAG1", "A");
List<Order> S = OrderBuilder.build(5, 4, "TAG2", "B");
List<Order> T = OrderBuilder.build(9, 4, "TAG3", "C");
ArrayList<Order> orders = new ArrayList<Order>() {{
addAll(F);
addAll(S);
addAll(T);
}};
for (Order order : orders) {
Message msg = new Message("transaction-topic", order.getTag(), order.toString().getBytes());
msg.setKeys("transaction-topic-trace");
// 当发生异常的时候我们模拟 id % 5的消息就算异常了事务也执行成功
if (order.getOrderID() % 5 == 0) {
msg.putUserProperty("state", String.valueOf(true));
}
producer.sendMessageInTransaction(msg, null);
}
}
/**
* 模拟操作DB 三种状态 成功 失败 发生异常
* @return
* @throws IllegalArgumentException
*/
public static boolean getState() throws IllegalArgumentException {
Random random = new Random();
int randomNumber = random.nextInt(3);
if (randomNumber == 0) {
return true;
} else if (randomNumber == 1) {
return false;
}
throw new IllegalArgumentException();
}
}
我们可以得到下面的日志:
============================ 检查事务状态 ===========================
================= 成功提交了消息:orderID 1 desc A tag TAG1 state 2024-01-09 10:59:35 sendTime
============================ 检查事务状态 ===========================
================= 回滚消息:orderID 2 desc A tag TAG1 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 发生了异常,要进行异常检查 orderID 3 desc A tag TAG1 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 回滚消息:orderID 4 desc A tag TAG1 state 2024-01-09 10:59:36 sendTime
java.lang.IllegalArgumentException
at com.liyong.learn.Producer.getState(Producer.java:99)
at com.liyong.learn.Producer$1.executeLocalTransaction(Producer.java:36)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendMessageInTransaction(DefaultMQProducerImpl.java:1264)
at org.apache.rocketmq.client.producer.TransactionMQProducer.sendMessageInTransaction(TransactionMQProducer.java:94)
at com.liyong.learn.Producer.main(Producer.java:79)
============================ 检查事务状态 ===========================
================= 回滚消息:orderID 5 desc B tag TAG2 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 成功提交了消息:orderID 6 desc B tag TAG2 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 成功提交了消息:orderID 7 desc B tag TAG2 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 回滚消息:orderID 8 desc B tag TAG2 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 成功提交了消息:orderID 9 desc C tag TAG3 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 发生了异常,要进行异常检查 orderID 10 desc C tag TAG3 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 发生了异常,要进行异常检查 orderID 11 desc C tag TAG3 state 2024-01-09 10:59:36 sendTime
============================ 检查事务状态 ===========================
================= 回滚消息:orderID 12 desc C tag TAG3 state 2024-01-09 10:59:36 sendTime
java.lang.IllegalArgumentException
at com.liyong.learn.Producer.getState(Producer.java:99)
at com.liyong.learn.Producer$1.executeLocalTransaction(Producer.java:36)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendMessageInTransaction(DefaultMQProducerImpl.java:1264)
at org.apache.rocketmq.client.producer.TransactionMQProducer.sendMessageInTransaction(TransactionMQProducer.java:94)
at com.liyong.learn.Producer.main(Producer.java:79)
java.lang.IllegalArgumentException
at com.liyong.learn.Producer.getState(Producer.java:99)
at com.liyong.learn.Producer$1.executeLocalTransaction(Producer.java:36)
at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendMessageInTransaction(DefaultMQProducerImpl.java:1264)
at org.apache.rocketmq.client.producer.TransactionMQProducer.sendMessageInTransaction(TransactionMQProducer.java:94)
at com.liyong.learn.Producer.main(Producer.java:79)
============================ 异常检查机制 ===========================
================= 异常检查通过,成功发送消息:orderID 10 desc C tag TAG3 state 2024-01-09 10:59:36 sendTime
============================ 异常检查机制 ===========================
============================ 异常检查机制 ===========================
这个日志我们分析知道模拟的事务操作成功执行了4次,分别为1,6,7,9 然后有3条发生了异常(不确定事务有没有执行成功)为3,10,11这个时候过一段时间就会进入checkLocalTransaction这个方法,我们的模拟逻辑是如果模于5为0则也提交消息。其它的消息事务都执行失败,所以消费者最终得到了1,6,7,9,10这几条消息进行后面的逻辑处理,这样可以看到保证了A这边的事务执行成功了,再到消费者去执行B后面的逻辑也要保证B成功否则还是需要回滚。RocketMQ保证了消费者不会受到A这边事务已经执行失败的消息。
消费者
public class Consumer {
public static void main(String[] args) throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction-topic-group");
consumer.setNamesrvAddr("ip:9876");
consumer.subscribe("transaction-topic", "*");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
System.out.println(String.format("msg {%s} recvTime %s %s", new String(msg.getBody()), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), msg.getTags()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
}
}