一.为什么选择RocketMQ消息队列?(可跳过看三的整合代码实例)
- 首先RocketMQ是阿里巴巴自研出来的,也已开源。其性能和稳定性从双11就能看出来,借用阿里的一句官方介绍:历年双 11
购物狂欢节零点千万级 TPS、万亿级数据洪峰,创造了全球最大的业务消息并发以及流转纪录(日志类消息除外);
- 在始终保证高性能前提下,支持亿级消息堆积,不影响集群的正常服务,在削峰填谷(蓄洪)、微服务解耦的场景下尤为重要;这,就能说明RocketMQ的强大。
二.RocketMQ的特点和优势(可跳过看三的整合代码实例)
- 削峰填谷(主要解决诸如秒杀、抢红包、企业开门红等大型活动时皆会带来较高的流量脉冲,或因没做相应的保护而导致系统超负荷甚至崩溃,或因限制太过导致请求大量失败而影响用户体验,海量消息堆积能力强)
- 异步解耦(高可用松耦合架构设计,对高依赖的项目之间进行解耦,当下游系统出现宕机,不会影响上游系统的正常运行,或者雪崩)
- 顺序消息(顺序消息即保证消息的先进先出,比如证券交易过程时间优先原则,交易系统中的订单创建、支付、退款等流程)
- 分布式事务消息(确保数据的最终一致性,大量引入 MQ 的分布式事务,既可以实现系统之间的解耦,又可以保证最终的数据一致性,减少系统间的交互)
三.SpringBoot 整合RocketMQ(商业云端版)
- 首先去阿里云控制台创建所需消息队列资源,包括消息队列 RocketMQ 的实例、Topic、Group ID (GID),以及鉴权需要的 AccessKey(AK)。
- 在springboot项目pom.xml添加需要的依赖
<!-- RocketMQ -->
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>ons-client</artifactId>
<version>1.8.0.Final</version>
</dependency>
- 在对应环境的application-xx.properties文件配置参数
##-------鉴权需要的 AccessKey(AK)(实际项目,这里填写阿里云自己的账号信息)---
rocketmq.accessKey=xxAxxxxxxxxxx
rocketmq.secretKey=xxxxxxxxxiHxxxxxxxxxxxxxx## 实例TCP 协议公网接入地址(实际项目,填写自己阿里云MQ的公网地址)
rocketmq.nameSrvAddr=http://MQ_INST_***********85_BbM********************yuncs.com:80
#普通消息topic (实际项目,填写自己阿里云MQ中的topic名称和groupid)
rocketmq.topic=common
rocketmq.groupId=GID-message
rocketmq.tag=*
#定时/延时消息
rocketmq.timeTopic=time-lapse
rocketmq.timeGroupId=GID-message
rocketmq.timeTag=*
@Configuration
@ConfigurationProperties(prefix = "rocketmq")
public class MqConfig {
private String accessKey;
private String secretKey;
private String nameSrvAddr;
private String topic;
private String groupId;
private String tag;
private String timeTopic;
private String timeGroupId;
private String timeTag;
public Properties getMqPropertie() {
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.AccessKey, this.accessKey);
properties.setProperty(PropertyKeyConst.SecretKey, this.secretKey);
properties.setProperty(PropertyKeyConst.NAMESRV_ADDR, this.nameSrvAddr);
properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, "4000");
return properties;
}
public String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getNameSrvAddr() {
return nameSrvAddr;
}
public void setNameSrvAddr(String nameSrvAddr) {
this.nameSrvAddr = nameSrvAddr;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public String getTimeTopic() {
return timeTopic;
}
public void setTimeTopic(String timeTopic) {
this.timeTopic = timeTopic;
}
public String getTimeGroupId() {
return timeGroupId;
}
public void setTimeGroupId(String timeGroupId) {
this.timeGroupId = timeGroupId;
}
public String getTimeTag() {
return timeTag;
}
public void setTimeTag(String timeTag) {
this.timeTag = timeTag;
}
}
- 给消息生产者注入配置信息,ProducerBean用于将Producer集成至Spring Bean中
@Configuration
public class ProducerClient {
@Autowired
private MqConfig mqConfig;
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ProducerBean buildProducer() {
ProducerBean producer = new ProducerBean();
producer.setProperties(mqConfig.getMqPropertie());
return producer;
}
}
- 为了方便使用,我封装了一个发送消息的类,消息的Message参数和配置,看代码注释,很容易理解
@Component
public class ProducerUtil {
private Logger logger = LoggerFactory.getLogger(ProducerUtil.class);
@Autowired
private MqConfig config;
@Autowired
private ProducerBean producer;
public SendResult sendMsg(String msgTag,byte[] messageBody,String msgKey) {
Message msg = new Message(config.getTopic(),msgTag,msgKey,messageBody);
return this.send(msg,Boolean.FALSE);
}
public SendResult sendTimeMsg(String msgTag,byte[] messageBody,String msgKey,long delayTime) {
Message msg = new Message(config.getTimeTopic(),msgTag,msgKey,messageBody);
msg.setStartDeliverTime(delayTime);
return this.send(msg,Boolean.FALSE);
}
public void sendOneWayMsg(String msgTag,byte[] messageBody,String msgKey) {
Message msg = new Message(config.getTopic(),msgTag,msgKey,messageBody);
this.send(msg,Boolean.TRUE);
}
private SendResult send(Message msg,Boolean isOneWay) {
try {
if(isOneWay) {
producer.sendOneway(msg);
success(msg, "单向消息MsgId不返回");
return null;
}else {
SendResult sendResult = producer.send(msg);
if (sendResult != null) {
success(msg, sendResult.getMessageId());
return sendResult;
}else {
error(msg,null);
return null;
}
}
} catch (Exception e) {
error(msg,e);
return null;
}
}
private ExecutorService threads = Executors.newFixedThreadPool(3);
public void sendAsyncMsg(String msgTag,byte[] messageBody,String msgKey) {
producer.setCallbackExecutor(threads);
Message msg = new Message(config.getTopic(),msgTag,msgKey,messageBody);
try {
producer.sendAsync(msg, new SendCallback() {
@Override
public void onSuccess(final SendResult sendResult) {
assert sendResult != null;
success(msg, sendResult.getMessageId());
}
@Override
public void onException(final OnExceptionContext context) {
error(msg,context.getException());
}
});
} catch (ONSClientException e) {
error(msg,e);
}
}
private void error(Message msg,Exception e) {
logger.error("发送MQ消息失败-- Topic:{}, Key:{}, tag:{}, body:{}"
,msg.getTopic(),msg.getKey(),msg.getTag(),new String(msg.getBody()));
logger.error("errorMsg --- {}",e.getMessage());
}
private void success(Message msg,String messageId) {
logger.info("发送MQ消息成功 -- Topic:{} ,msgId:{} , Key:{}, tag:{}, body:{}"
,msg.getTopic(),messageId,msg.getKey(),msg.getTag(),new String(msg.getBody()));
}
}
- 注入封装的发送消息util类,在业务系统需要的地方调用来发送消息即可
@Autowired
private ProducerUtil producer;
public String mqTest() {
JSONObject body = new JSONObject();
body.put("userId", "this is userId");
body.put("notice", "同步消息");
producer.sendMsg("userMessage", body.toJSONString().getBytes(), "messageId");
producer.sendOneWayMsg("userMessage", "单向消息".getBytes(), "messageId");
producer.sendAsyncMsg("userMessage", "异步消息".getBytes(), "messageId");
producer.sendTimeMsg("userMessage", "延时消息".getBytes(), "messageId", System.currentTimeMillis()+30000);
return "ok";
}
- 接下来是消息消费者的配置和接收消息(一般在下游系统或者相关联的系统),接收消息的项目照旧,添加依赖jar包 ons-client v1.8.0.Final 、配置mq参数链接(mq的配置文件参数要和生产者项目配置的一样)、添加MqConfig类(上面有写)
- 注入配置、订阅消息、添加消息处理的方法
@Configuration
public class ConsumerClient {
@Autowired
private MqConfig mqConfig;
@Autowired
private MqMessageListener messageListener;
@Autowired
private MqTimeMessageListener timeMessageListener;
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ConsumerBean buildConsumer() {
ConsumerBean consumerBean = new ConsumerBean();
Properties properties = mqConfig.getMqPropertie();
properties.setProperty(PropertyKeyConst.GROUP_ID, mqConfig.getGroupId());
properties.setProperty(PropertyKeyConst.ConsumeThreadNums, "20");
consumerBean.setProperties(properties);
Map<Subscription, MessageListener> subscriptionTable = new HashMap<Subscription, MessageListener>();
Subscription subscription = new Subscription();
subscription.setTopic(mqConfig.getTopic());
subscription.setExpression(mqConfig.getTag());
subscriptionTable.put(subscription, messageListener);
Subscription subscriptionTime = new Subscription();
subscriptionTime.setTopic(mqConfig.getTimeTopic());
subscriptionTime.setExpression(mqConfig.getTimeTag());
subscriptionTable.put(subscriptionTime, timeMessageListener);
consumerBean.setSubscriptionTable(subscriptionTable);
return consumerBean;
}
}
@Component
public class MqTimeMessageListener implements MessageListener {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Action consume(Message message, ConsumeContext context) {
logger.info("接收到MQ消息 -- Topic:{}, tag:{},msgId:{} , Key:{}, body:{}", message.getTopic(),message.getTag(),message.getMsgID(),message.getKey(),new String(message.getBody()));
try {
String msgTag = message.getTag();
String msgKey = message.getKey();
switch (msgTag) {
case "userMessage":
if(
break;
}
break;
}
return Action.CommitMessage;
} catch (Exception e) {
logger.error("消费MQ消息失败! msgId:" + message.getMsgID()+"----ExceptionMsg:"+e.getMessage());
return Action.ReconsumeLater;
}
}
}
@Component
public class MqMessageListener implements MessageListener {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Action consume(Message message, ConsumeContext context) {
logger.info("接收到MQ消息. Topic :" + message.getTopic() + ", tag :" + message.getTag()+ " msgId : " + message.getMsgID()+", Key :" + message.getKey()+", body:" + new String(message.getBody()));
try {
String msgTag = message.getTag();
String msgKey = message.getKey();
switch (msgTag) {
case "userMessage":
break;
}
return Action.CommitMessage;
} catch (Exception e) {
logger.error("消费MQ消息失败! msgId:" + message.getMsgID()+"----ExceptionMsg:"+e.getMessage());
return Action.ReconsumeLater;
}
}
}
四.最后运行消费者项目和生产者项目,调用生产者项目发送消息验证效果:
2019-08-17 15:11:06.837 INFO 9996 --- [nio-8080-exec-9] com.dyj.shop.mq.ProducerUtil : 发送MQ消息成功 -- Topic:common ,msgId:C0A86532270C2A139A5555A7E5DD0000 , Key:messageId, tag:userMessage, body:{"userId":"this is userId","notice":"同步消息"}
2019-08-17 15:11:06.841 INFO 9996 --- [nio-8080-exec-9] com.dyj.shop.mq.ProducerUtil : 发送MQ消息成功 -- Topic:common ,msgId:单向消息MsgId不返回 , Key:messageId, tag:userMessage, body:单向消息
2019-08-17 15:11:06.901 INFO 9996 --- [pool-6-thread-1] com.dyj.shop.mq.ProducerUtil : 发送MQ消息成功 -- Topic:common ,msgId:C0A86532270C2A139A5555A7E6630004 , Key:messageId, tag:userMessage, body:异步消息
2019-08-17 15:11:07.060 INFO 9996 --- [nio-8080-exec-9] com.dyj.shop.mq.ProducerUtil : 发送MQ消息成功 -- Topic:time-lapse ,msgId:C0A86532270C2A139A5555A7E69F0006 , Key:messageId, tag:userMessage, body:定时/延时消息
- 消费者接收到消息,可以看到普通消息的发送时间和接收到消息的时间,就相差几毫秒,值得注意的是:延时消息按照生产者定义的30秒后消费者才收到。这就是延时消息的好玩之处
2019-08-17 15:11:06.881 INFO 10942 --- [MessageThread_7] com.dyj.timer.mq.MqMessageListener : 接收到MQ消息. Topic :common, tag :userMessage msgId : C0A86532270C2A139A5555A7E5DD0000, Key :messageId, body:{"userId":"this is userId","notice":"同步消息"}
2019-08-17 15:11:06.934 INFO 10942 --- [MessageThread_8] com.dyj.timer.mq.MqMessageListener : 接收到MQ消息. Topic :common, tag :userMessage msgId : C0A86532270C2A139A5555A7E6550002, Key :messageId, body:单向消息
2019-08-17 15:11:06.947 INFO 10942 --- [MessageThread_9] com.dyj.timer.mq.MqMessageListener : 接收到MQ消息. Topic :common, tag :userMessage msgId : C0A86532270C2A139A5555A7E6630004, Key :messageId, body:异步消息
2019-08-17 15:11:36.996 INFO 10942 --- [essageThread_10] com.dyj.timer.mq.MqTimeMessageListener : 接收到MQ消息. Topic :time-lapse, tag :userMessage msgId : cd900e16f7cba68369ec498ae2f9dd6c, Key :messageId, body:定时/延时消