1.简介
1.1. 什么是消息中间件
简单来说就是存在于客户端和服务端一种存储技术
1.2. 消息中间件的作用是什么
存储海量的消息(比如每当双十一、双十二这时用户访问服务器的会比较频繁、这时服务器压力会很大,短时间可能处理不了那么多的请求,这时消息中间件就站了出来他会把所有消息存储在自己身上,服务器每次消费多少消息就从消息中间件这里取多少消息,从而达到限流、削峰的效果)。
比如注册时注册邮件和发送短信同时发送(异步化提高了效率)
2. 那么多的消息队列我们为什么要使用RocketMq
2.1 ActiveMQ
最早出来的消息中间件,在很多年前他很流行,但现在他已经过时了。
1.它的社区很不活跃,如果出现bug可能要过很长时间才能解决。
2.它在及其偶然的情况下可能会丢消息。
3.它的吞吐量以及他的可用性都比不上其他消息中间件
2.2 RabbitMQ
2.3 Kafka
1.大数据用的比较多(批处理)
2.支持分布式处理能力很强但它的功能单一
3.RocketMQ为什么会这么强大(内存模型)
3.1 基本概念
- Producer: 消息生产者,负责消息的产生,由业务系统负责产生
- Consumer:消息消费者,负责消息消费,由后台业务系统负责异步消费
- Topic:消息的逻辑管理单位
这三者是Rocket最基本的概念Producer负责消息的生产、Consumer负责消息的消费、Topic负责消息的存储(三者相辅相成)。具体来说是Producer将消息发往具体的Topic。Consumer订阅Topic,主动拉取或被动接受消 息,如果Consumer消费消息失败则默认会重试16次
3.2 深入了解
- Broker:负责消息的中转功能(邮局) ,里面有多个Topic。
-
MessageQueue: 消息的物理管理单位,一个Topic下有多个Queue,默认一个Topic创建时会创建 四个MessageQueue。
-
ConsumerGroup:具有同样消费逻辑消费同样消息的Consumer,可以归并为一个group。
-
ProducerGroup: 具有同样属性的一些Producer可以归并为同一个Group。
同样属性是指:发送同样Topic类型的消息。
-
Nameserver 注册中心( 每个Broker启动的时候会向namesrv注册、Producer发送消息的时候根据Topic获取路由到Broker里面Broker的信息、Consumer根据Topic到Namesrv 获取topic的路由到Broker的信息) 。
3.3 更上一层楼(部署模型)
执行步骤:
1、注册中心Nameserver启动
2、消息中转服务Broker启动
- 启动的时候会去创建Topic并创建对应的MessageQueue
- 启动的时候会去注册中心注册,把自己的地址以及负责的Topic告诉注册中心
- Broker和Nameserver之间通过心跳机制来检测对方是否存活
启动时:
- 单个生产者者和一台nameserver保持长连接,定时查询topic配置信息
- 单个生产者和该生产者关联的所有broker保持长连接。
- 运行时:
- 默认情况下,生产者每隔30秒从nameserver获取所有topic的最新队列情况
- 发送消息时,根据从nameserver获取的路由信息,根据发送消息的Topic和目标 Broker建立连接
- 默认情况下,生产者每隔30秒向所有broker发送心跳,该时间由DefaultMQProducer的 heartbeatBrokerInterval参数决定,可手动配置。broker每隔10秒钟(此时间无法更改), 扫描所有还存活的连接,若某个连接2分钟内(当前时间与最后更新时间差值超过2分钟,此时间无法更改)没有发送心跳数据,则关闭连接
-
4. 消息消费者 Consumer 启动
-
启动时:单个消费者和一台 nameserver 保持长连接,定时查询 topic 配置信息单个消费者和该消费者关联的所有 broker 保持长连接。运行时 :默认情况下,消费者每隔 30 秒从 nameserver 获取所有 topic 的最新队列情况默认情况下,消费者每隔 30 秒向所有 broker 发送心跳,该时间由 DefaultMQPushConsumer的 heartbeatBrokerInterval 参数决定,可手动配置。 broker 每隔 10 秒钟(此时间无法更改),扫描所有还存活的连接,若某个连接 2 分钟内(当前时间与最后更新时间差值超过 2 分钟,此时间无法更改)没有发送心跳数据,则关闭连接,并向该消费者分组的所有消费者发出通知,分组内消费者重新分配队列继续消费
3.4 刷盘策略和复制策略
-
同步刷盘与异步刷盘 :RocketMQ 的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ 为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过 Producer 写入 RocketMQ 的时候,有两种写磁盘方式:异步刷盘 :在返回写成功状态时,消息可能只是被写入了内存中,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入同步刷盘 :在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。同步刷盘还是异步刷盘,是通过 Broker 配置文件里的 flushDiskType 参数设置的,这个参数被设置成 SYNC_FLUSH 、 ASYNC_FLUSH中的一个
-
同步复制与异步复制如果一个 broker 组有 Master 和 Slave ,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式。同步复制是等 Master 和 Slave 均写成功后才反馈给客户端写成功状态;异步复制方式是只要 Master写成功即可反馈给客户端写成功状态同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的,这个参数可以被设置成ASYNC_MASTER 、 SYNC_MASTER 、 SLAVE 三个值中的一个
4. 安装使用
4.1 下载
4.2 安装配置
配置
4.3 启动
为了防止内存不足(配置内存大小)打开bin目录下的这两个文件
修改NameServer和Broker配置
启动
Windows
- 启动注册中心nameServer,默认启动在9876端口,打开cmd命令窗口,进入bin目录,执行
命令
start mqnamesrv.cmd
Linux执行
sh ./mqnamesrv
出现以下日志表示启动成功
启动RocketMQ服务,也就是broker进入bin目录,执行命令:
Windows
start mqbroker.cmd -n 127 .0.0.1:9876 autoCreateTopicEnable = true
Linux
sh ./mqbroker -n 127 .0.0.1:9876 autoCreateTopicEnable = true
出现以下日志表示启动成功
4.4 配置文件
我们在 windows 下安装 RocketMQ ,消息数据默认会被存储在 c 盘,如果有同学 c 盘已经满了,那么此时 我们可以修改数据的存储路径到任意位置,通过在配置文件中添加如下配置即可:这里稍微注意下,如 果是windows 的路径,那么目录与目录之间用 / 而不是 \ 分割,比如 d:/store/...
# 存储路径storePathRootDir = /usr/local/rocketmq/store#commitLog 存储路径storePathCommitLog = /usr/local/rocketmq/store/commitlog# 消费队列存储路径存储路径storePathConsumeQueue = /usr/local/rocketmq/store/consumequeue# 消息索引存储路径storePathIndex = /usr/local/rocketmq/store/index#checkpoint 文件存储路径storeCheckpoint = /usr/local/rocketmq/store/checkpoint#abort 文件存储路径abortFile = /usr/local/rocketmq/store/abort
5. 项目中使用
5.1 导包
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
5.2 普通消息
- 消息生产者
public static void main(String[] args){
// 新增消息生产者
DefaultMQProucer producer = new DefaultMQProucer("producer_group");
// 配置注册中心
producer.setNamesrvAddr("localhost:9876");
// 启动
producer.start();
// 新建消息对象
Message message = new Message("topicA","message".context.getBytes(Charset.forName("utf-8")));
// 发送消息
producer.send(message);
}
- 消息消费者
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer mqConsumer = new DefaultMQPushConsumer("consumer_group"); mqConsumer.setNamesrvAddr("localhost:9876");
mqConsumer.subscribe("topicA", "*");
// 设置消息监听器
mqConsumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { MessageExt message = msgs.get(0);
//获取消息内容
byte[] body = message.getBody(); });
mqConsumer.start();
5.3 延迟消息
-
消息生产者
-
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException { // 1. 创建Producer对象 DefaultMQProducer produce = new DefaultMQProducer("delay_producer_group"); producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); // 准备消息 Message message = new Message(); message.setTopic("test_delay"); message.setBody("hello,delay".getBytes("utf-8")); // 非常简单 延迟级别 // 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 message.setDelayTimeLevel(2); // 发送 SendResult send = producer.send(message); System.out.println(send); }
-
消息消费者 ( 和普通的消息消费者没有区别 )
5.4 项目中使用
在项目中,我们基于 RocketMQ 的延迟消息,实现订单超时自动取消的功能- 在下单的最后一步,即SendMessageHandler中,发送延时消息(消息的内容主要包含订单id),消 息的延迟时间,即订单支付超时的时间(比如半小时)
- 在定义一个消费者,专门消费这个延迟消息,在会在订单超时时间到了之后,获取到消息中的订单 id,检查订单的状态,若未支付则,取消订单
- 取消订单对应的逻辑,即修改订单的状态为取消订单,并根据订单中购买的各订单商品条目的数 量,扣减锁定库存,增加可售卖库存(即还原库存)
- 因为每一个成功下单的的订单,都会对应一个这样的延迟消息,所以这意味着对于每一个订单,都会做超时自动取消的检查
-
5.5 项目中实现
@Component
@Slf4j
public class SendMessageHandler extends AbstractTransHandler {
// 注入我们自己定义的Producer对象
@Autowired
DelyOrderCancelProducer producer;
@Override
public boolean isAsync() {
return false; }
@Override
public boolean handle(TransHandlerContext context) {
// 当代码执行到这里,下单已经成功了,针对已经下单成功的订单,发送延迟消息(延迟时间就是 订单的超时时间)
CreateOrderContext createOrderContext = (CreateOrderContext) context;
// 从上下文对象中获取订单Id
String orderId = createOrderContext.getOrderId();
// 将orderId,放入延迟消息中发送(调用我们自己定义的producer对象)
return producer.sendOrderCancelMessage(orderId);
}
}
@Component
@Slf4j
public class DelyOrderCancelProducer {
private DefaultMQProducer producer;
@PostConstruct
public void init() {
// 1. 创建Producer对象
producer = new DefaultMQProducer("order_delay_cancel_producer");
// 2. 设置nameserv地址
producer.setNamesrvAddr("127.0.0.1:9876");
try {
// 启动 producer.start();
} catch (MQClientException e) {
e.printStackTrace(); }
}
// 返回值表示延迟消息发送的结果
public boolean sendOrderCancelMessage(String orderId) {
// 千万不要忘了添加日志
// 准备待发送的消息 Message message = new Message(); message.setTopic("delay_order_cancel"); message.setBody(orderId.getBytes(Charset.forName("utf-8")));
// 设置延迟消息的延迟级别
//message.setDelayTimeLevel(16);
message.setDelayTimeLevel(3);
SendResult sendResult = null;
Exception ex = null;
try {
sendResult = producer.send(message);
} catch (MQClientException e) {
e.printStackTrace(); ex = e; }
catch (RemotingException e) {
e.printStackTrace(); ex = e;
} catch (MQBrokerException e)
{ e.printStackTrace(); ex = e;
} catch (InterruptedException e) {
e.printStackTrace(); ex = e;
}
if (ex != null) {
// 发送时出现异常,发送失败 return false; }
if (sendResult != null && SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
// 消息发送成功
return true; }
// 消息发送失败
return false;
}
}
@Component
public class DelayOrderCancelConsumer {
private DefaultMQPushConsumer consumer;
@PostConstruct
public void init() {
consumer = new DefaultMQPushConsumer("order_delay_cancel_consumer"); consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.setMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// 实现,订单超时自动取消的业务逻辑
MessageExt message = msgs.get(0);
// 1. 取出orderId byte[] body = message.getBody();
try {String orderId = new String(body, 0, body.length, "utf-8");
//2. 去数据库根据OrderId,查询订单转态
//3. 如果订单已经支付,或者已经取消,什么都不做
//4. 如果订单仍然是初始化转态,未支付成功,执行自动取消的逻辑
// a. 修改订单的状态为已取消
// b. 访问tb_order_item,查询订单中包含的所有订单商品条目,根据 订单条目中商品购买的数量 +stockCount -lockCount
} catch (UnsupportedEncodingException e) {
e.printStackTrace(); }return null; } });
try {consumer.subscribe("delay_order_cancel", "*");
consumer.start(); }
catch (MQClientException e)
{ e.printStackTrace();
}
}
}