RocketMQ初步了解,介绍安装部署实现短信服务请求的简版全流程。这里以Windows系统环境为例,主要做开发本地环境用,服务器(Linux)环境安装另述。
一、为什么需要消息队列
消息队列实际是一种单向队列的数据结构,特点是先进先出。同样需要考虑高并发流量、单点故障等应用场景。
-
应用解耦
举例:订单系统用户下单,同时还需要去库存系统中做库存变更、去积分系统做积分变更等,若同步执行的话很容易因为下游系统崩溃导致整个下单流程崩溃。
采用消息队列则可以在用户下单的时候直接返回一个下单成功的消息,让下游系统去订阅该消息即可。
-
流量削峰
将瞬时流量暂存起来,通过消息队列稳定进行消费。
-
消息分发
结合应用解耦的举例,引入消息队列之后可以让后续其他系统订阅下单发出的消息,例如目前需要发送下单成功的短信提醒,则将短信业务去订阅下单消息,后续若增加了邮箱通知,则可以让邮箱业务去订阅下单消息。
二、什么是RocketMQ
是java语言开发的一款分布式消息中间件。
由以下四个部分组成:
- nameServer
- brokerServer
- producer
- consumer
其中nameServer、brokerServer是RocketMQ的服务端,两者一起独立的对外提供服务。producer、consumer则是RocketMQ的客户端。
-
Name Server:名称服务充当路由消息的提供者。是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。在消息队列 RocketMQ 中提供命名服务,更新和发现 Broker 服务。主要有以下两个功能:
- 接收broker的请求,注册broker的路由信息
- 接收client(producer/consumer)的请求,根据某个topic获取其到broker的路由信息
NameServer没有状态,可以横向扩展。每个broker在启动的时候会到NameServer注册;Producer在发送消息前会根据topic到NameServer获取路由(到broker)信息;Consumer也会定时获取topic路由信息。
-
Broker:消息中转角色,负责存储消息,转发消息。可以理解为消息队列服务器,提供了消息的接收、存储、拉取和转发服务。broker是RocketMQ的核心,它不不能挂的,所以需要保证broker的高可用。
broker分为 Master Broker 和 Slave Broker,一个 Master Broker 可以对应多个 Slave Broker,但是一个 Slave Broker 只能对应一个 Master Broker。
Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。Broker 启动后需要完成一次将自己注册至 Name Server 的操作;随后每隔 30s 定期向 Name Server 上报 Topic 路由信息。
-
producer(生产者):与 Name Server 集群中的其中一个节点(随机)建立长链接(Keep-alive),定期从 Name Server 读取 Topic 路由信息,并向提供 Topic 服务的 Master Broker 建立长链接,且定时向 Master Broker 发送心跳。
-
consumer(消费者):与 Name Server 集群中的其中一个节点(随机)建立长连接,定期从 Name Server 拉取 Topic 路由信息,并向提供 Topic 服务的 Master Broker、Slave Broker 建立长连接,且定时向 Master Broker、Slave Broker 发送心跳。Consumer 既可以从 Master Broker 订阅消息,也可以从 Slave Broker 订阅消息,订阅规则由 Broker 配置决定。
在broker中也有几个关键属性,例如Topic和Queue:
三、安装启动RocketMQ
-
下载所需版本的bin-release包(选择Binary)
http://rocketmq.apache.org/dowloading/releases/
-
解压配置环境变量
- ROCKETMQ_HOME(配置解压后的Rocketmq根目录)
- JAVA_HOME(需要依赖jdk环境)
-
启动nameServer
进入到bin目录下打开cmd(最好保证是管理员权限),执行以下命令
mqnamesrv.cmd
- 启动broker
开启一个新的cmd,执行以下命令,表示在9876端口下启动broker
mqbroker.cmd -n localhost:9876
当nameServer和broker都启动完成后,rocketmq的服务端就已经可以对外提供服务了。
RocketMQ的开发人员在下载的包里已经内置了一套简单的demo消息收发测试程序,以下展示用例,如果不需要用例可以跳过:
-
启动consumer
同样在RocketMQ的bin目录下开启一个cmd命令窗口,首先在该窗口下设定命令行窗口级别的环境变量:
set NAMESRV_ADDR=localhost:9876
然后执行以下命令:
tools.cmd org.apache.rocketmq.example.quickstart.Consumer
-
启动producer
同样在RocketMQ的bin目录下开启一个cmd命令窗口,首先在该窗口下设定命令行窗口级别的环境变量:
set NAMESRV_ADDR=localhost:9876
然后执行以下命令:
tools.cmd org.apache.rocketmq.example.quickstart.Producer
如果成功的话,就能看到一口气发送了1000条普通消息,命令窗口中将会被日志信息刷屏,表示我们启动成功了。
接下来介绍如何在SpringBoot中集成RocketMQ。
四、结合SpringBoot
首先保证本地RocketMQ的nameServer、broker启动,在RocketMQ的bin目录下执行:
mqnamesrv.cmd
mqbroker.cmd -n localhost:9876
新建一个SpringBoot项目,采用最小依赖原则,我们主要用到RocketMq和lombok的依赖。
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- rocketmq -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
配置文件application.yaml:
rocketmq:
nameServer: 127.0.0.1:9876
producer:
group: sms-group
consumer:
group: sms-group
新建配置类,主要配置Name Server 地址和主题名称:
public class JmsConfig {
/**
* Name Server 地址,因为是集群部署 所以有多个用 分号 隔开
*/
public static final String NAME_SERVER = "127.0.0.1:9876";
/**
* 主题名称 主题一般是服务器设置好 而不能在代码里去新建topic( 如果没有创建好,生产者往该主题发送消息 会报找不到topic错误)
*/
public static final String TOPIC_SMS_SEND = "sms-send";
}
新建短信发送实体类:
@Data
public class SmsInfoDO {
private String phone;
private String name;
}
新建短信消费类:
@Slf4j
@Component
@RocketMQMessageListener(topic = TOPIC_SMS_SEND, consumerGroup = "sms-group")
public class SmsSendConsumer implements RocketMQListener<SmsInfoDO> {
@Override
public void onMessage(SmsInfoDO smsInfoDO) {
log.info("[短信发送消费者]received message:{}", smsInfoDO);
log.info("[短信发送消费者]模拟发送,发送中......");
log.info("[短信发送消费者]发送成功");
}
}
新建Controller进行测试,主要有三种发送方式,如下:
@Slf4j
@RestController
@RequestMapping("/mq")
public class MqMessageController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 同步发送
* @param req
* @return
*/
@GetMapping("/syncGo")
public boolean syncGo(SmsInfoDO req) {
log.info("[短信发送生产者-同步]开始推送给mq......");
rocketMQTemplate.syncSend(TOPIC_SMS_SEND, req);
log.info("[短信发送生产者-同步]已推送给mq");
return true;
}
/**
* 异步发送
* @param req
* @return
*/
@GetMapping("/asyncGo")
public boolean asyncGo(SmsInfoDO req){
rocketMQTemplate.asyncSend(TOPIC_SMS_SEND, req, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("[短信发送生产者-异步]已被mq消费!{}", sendResult);
}
@Override
public void onException(Throwable throwable) {
log.info("[短信发送生产者-异步]发送失败!{}", throwable.getMessage());
}
});
return true;
}
/**
* 采用one-way发送模式发送消息
* 存在消息丢失的风险,所以其适用于不重要的消息发送,比如日志收集
* @param req
* @return
*/
@GetMapping("/oneWayGo")
public boolean oneWayGo(SmsInfoDO req){
log.info("[短信发送生产者-one-way]开始推送给mq......");
rocketMQTemplate.sendOneWay(TOPIC_SMS_SEND, req);
log.info("[短信发送生产者-one-way]已推送给mq");
return true;
}
}
总结:
- 当发送的消息不重要时,采用
one-way
方式,以提高吞吐量; - 当发送的消息很重要是,且对响应时间不敏感的时候采用
sync
方式; - 当发送的消息很重要,且对响应时间非常敏感的时候采用
async
方式;
Talk is cheap,show me the code.
https://github.com/Vainycos/my_rocketmq
Tips:
需要注意以上实现只能作为示例,均未做消息幂等处理,不能排除消息被重复消费的情况。后续抽空将会在项目中实现相关幂等处理。
参考资料: