消息队列RocketMQ入门实践(一)
消息队列RocketMQ入门实践(二)
嗨,大家好,我是希留。
经过前面两篇文章的学习,相信大家对RocketMQ已经有了一个基本的了解了,这篇文章就来说一说RocketMQ的几个关键特性,废话不多说,咱开始吧。
项目源码:Gitee地址
在有的业务中,consumer在消费消息时,是需要按照生产者发送消息的顺序进行消费的,比如在电商系统中,订单的消息,会有创建订单、订单支付、订单完成,如果消息的顺序发生改变,那么这样的消息就没有意义了。
第一点,消息顺序发送,多线程发送的消息无法保证有序性,因此,需要业务方在发送时,针对同一个业务编号(如同一笔订单)的消息需要保证在一个线程内顺序发送,在上一个消息发送成功后,在进行下一个消息的发送。对应到mq中,消息发送方法就得使用同步发送,异步发送无法保证顺序性
第二点,消息顺序存储,mq的topic下会存在多个queue,要保证消息的顺序存储,同一个业务编号的消息需要被发送到一个queue中。对应到mq中,需要使用MessageQueueSelector来选择要发送的queue,即对业务编号进行hash,然后根据队列数量对hash值取余,将消息发送到一个queue中
第三点,消息顺序消费,要保证消息顺序消费,同一个queue就只能被一个消费者所消费,因此对broker中消费队列加锁是无法避免的。同一时刻,一个消费队列只能被一个消费者消费,消费者内部,也只能有一个消费线程来消费该队列。即,同一时刻,一个消费队列只能被一个消费者中的一个线程消费
生产者代码示例如下:
public class OrderProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("xiliu_producer_group");
producer.setNamesrvAddr("42.194.222.32:9876");
producer.start();
for (int i = 0; i<100; i++) {
String msgStr = "order -->" + i;
// 模拟生产订单id
int orderId = i % 10;
Message message = new Message("broker-a","order_msg",msgStr.getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(message,(mqs, msg, arg) -> {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}, orderId);
System.out.print(sendResult);
}
producer.shutdown();
}
}
消费者代码示例如下:
public class OrderConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("xiliu_consumer_group");
consumer.setNamesrvAddr("42.194.222.32:9876");
// 订阅topic,接收此Topic下的order_msg的消息
consumer.subscribe("broker-a", "order_msg");
// 使用顺序消息监听器
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("queueId:" + msg.getQueueId() + ",orderId:" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
}
}
因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
RocketMQ有一个比较关键的特性是,它支持事务消息。事务想必知道关系型数据的库的朋友应该比较了解,不了解的朋友也没有关系,咱往下继续。
聊什么是事务,最经典的例子就是转账操作,用户A转账给用户B100元的过程如下:
如果,用户A账户减去100元后,出现了故障(如网络故障),那么需要将该操作回滚,用户A账户增加100元。这就是事务。
随着项目越来越复杂,越来越服务化,就会导致系统间的事务问题,这个就是分布式事务问题。
分布式事务分类有这几种:
解决分布式事务问题的方案有很多,使用消息实现只是其中的一种。
RocketMQ实现分布式事务消息是采用的两阶段提交的补偿型方案,即:发送半消息(Half Message)和消息回查(Message Status Check)
Half(Prepare) Message
指的是暂不能投递的消息,发送方已经将消息成功发送到了 MQ 服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半消息。
Message Status Check
由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,MQ 服务端通过扫描发现某条消息长期处于“半消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该过程即消息回查。
下面我们来看一个简单的例子:
生产者
public class TransactionProducer {
public static void main(String[] args) throws Exception{
TransactionMQProducer producer = new TransactionMQProducer("xiliu_producer_group");
producer.setNamesrvAddr("42.194.222.32:9876");
// 设置超时时间
producer.setSendMsgTimeout(8000);
// 设置事物监听器
producer.setTransactionListener(new TransactionListenerImpl());
producer.start();
// 发消息
Message message = new Message("broker-a","transaction_msg","用户A给用户B转账500元".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(message,null);
System.out.printf("%s%n", sendResult);
producer.shutdown();
}
}
事物监听器实现类TransactionListenerImpl
public class TransactionListenerImpl implements TransactionListener{
private static Map<String, LocalTransactionState> STATE_MAP = new HashMap<>();
/**
* 执行具体的业务逻辑
**/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
try{
// 模拟调用服务
System.out.println("用户A账户减500元");
Thread.sleep(500);
// 模拟出现异常,返回回滚状态
System.out.println(1/0);
System.out.println("用户B账户加500元");
Thread.sleep(500);
STATE_MAP.put(message.getTransactionId(),LocalTransactionState.COMMIT_MESSAGE);
// 二次提交确认
return LocalTransactionState.COMMIT_MESSAGE;
}catch (Exception e){
e.printStackTrace();
}
STATE_MAP.put(message.getTransactionId(),LocalTransactionState.ROLLBACK_MESSAGE);
// 回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
}
/**
* 消息回查
**/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
return STATE_MAP.get(messageExt.getTransactionId());
}
}
消费者
public class TransactionConsumer {
public static void main(String[] args) throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("xiliu_consumer_group");
consumer.setNamesrvAddr("42.194.222.32:9876");
// 订阅事物消息tag
consumer.subscribe("broker-a","transaction_msg");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list) {
try {
System.out.println(new String(msg.getBody(), "UTF-8"));
}catch (Exception e) {
e.printStackTrace();
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
经过测试,返回commit状态时,消费者能够接收到消息,返回rollback状态时,消费者接收不到消息。
感谢大家的阅读,以上就是今天要讲的内容,本文介绍了RocketMQ的关键特性里面的顺序消息和事物消息的实现原理及代码示例,而RocketMQ还有很多的特性,我们下篇文章在来说一说,敬请期待。
若觉得本文对你有帮助的话,帮忙点赞评论关注,支持一波哟~