文章目录
一、MQ的概念:
消息模型:
- 🍋
RocketMQ
主要由Producer
、Broker
、Consumer
三部分组成 其中Producer
负责生产消息,Consumer
负责消费消息,Broker
负责存储消息。Broker
在实际部署过程中对应一台服务器,每个Broker
可以存储多个Topic
的消息,每个Topic
的消息也可以分片存储于不同的Broker
。Message Queue
用于存储消息的物理地址,每个Topic
中的消息地址存储于多个Message Queue
中。ConsumerGroup
由多个Consumer
实例构成。 - 🍋
Producer
:- 负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到
broker
服务器。RocketMQ
提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker
返回确认信息,单向发送不需要。
- 负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到
- 🍋
Consumer
:- 负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从
Broker
服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。
- 负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从
- 🍋
Broker
:- 负责存储消息,生产者发送的消息存储在这里而这里则是由多个队列组成,主要是担任中转、代理的角色。
MQ的优缺点:
优点:
- 🥭应用解耦,客户端不直接和服务端打交道
- 🥭适应应用的快速变更,服务器数量的变更
- 🥭流量削峰,突发的流量不会直接打到服务端
缺点:
- 🥭系统可用性降低,一旦
MQ
挂掉客户端发送不了消息和服务端拉去不到消息 - 🥭系统负责度提高,增加了一项技术自然复杂度提高了
- 🥭异步消息机制
- 1️⃣ 消息的顺序性
- 2️⃣ 消息的丢失
- 3️⃣ 消息的一致性
- 4️⃣ 消息重复使用
MQ的产品分类:
名称 | 开发语言 | 吞吐量 | 处理时间 | 架构 | 亮点 |
---|---|---|---|---|---|
ActiveMQ | Java | 万级 | ms级 | 主从 | 成熟度高 |
RabbitMQ | Erlang | 万级 | us级 | 主从 | 性能更好 |
RocketMQ | Java | 十万级 | ms级 | 分布式 | 扩展性强 |
kafka | Scala | 十万 | ms级 | 分布式 | 多应用于大数据 |
👉RocketMQ完美解决MQ的缺点
二、环境搭建:
🍂下载:
🔗下载地址:https://rocketmq.apache.org/docs/quick-start/
🍂版本支持:
🍂安装:
-
JDK1.8
我想这是必要条件 -
上传到
Liunx
后解压即可 -
启动
Name Server
nohup sh bin/mqnamesrv & tail -f ~/logs/rocketmqlogs/namesrv.log The Name Server boot success...
-
启动
Broker
nohup sh bin/mqbroker -n localhost:9876 & tail -f ~/logs/rocketmqlogs/broker.log The broker[%s, 172.30.30.233:10911] boot success...
-
如果你在启动的时候出现这个错:
- 那么你需要配置下
JVM
的给与Broker
的内存大小,/bin/runbroker.sh
三、消息发送:
1️⃣ 导入客户端依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.3</version>
</dependency>
2️⃣ 编写生成者Producer
public class Producer {
public static void main(String[] args) throws Exception {
// 1.创建mq对象
DefaultMQProducer producer = new DefaultMQProducer();
//2.设置组
producer.setProducerGroup("group");
//3.设置nameServer的地址
producer.setNamesrvAddr("localhost:9876");
//4.开启发送
producer.start();
//5.创建消息对象
Message message = new Message("topic1","发送的消息".getBytes(StandardCharsets.UTF_8));
//6.发送消息
SendResult sendResult = producer.send(message); //指定超时时间
System.out.println(sendResult);
//7.关闭连接
producer.shutdown();
}
}
3️⃣ 编写消费者Consumer
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 1.创建mq对象
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");
//2.设置nameServer的地址
consumer.setNamesrvAddr("localhost:9876");
//3.设置topic以及flag
consumer.subscribe("topic1","*");
//4.注册一个监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("接收到的消息:" + new String(messageExt.getBody()));
System.out.println(messageExt);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//.开启发送
consumer.start();
}
}
🍀如果你启动
Producer
报如下错误,你只需要在发送消息的时候指定超时时间即可
🍀 如果任然出现和我一样的错,并且你使用的是云服务器,那么你需要开放9876和10911端口,如果你使用的虚拟机那么你应该关闭防火墙
🍀 如果你发现启动broker的时候ip发生了变化,那么你可以固定你想要的ip来进行启动
🍀然后启动broker时输入:
nohup ./mqbroker -n 自己指定的ip:9876 -c …/conf/broker.conf & 即可
发送消息的类型:
- 同步消息:上面的例子便是同步。
- 异步消息:
public class AsyncSendMessage {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
// 1.创建mq对象
DefaultMQProducer producer = new DefaultMQProducer();
//2.设置组
producer.setProducerGroup("group");
//3.设置nameServer的地址
producer.setNamesrvAddr("localhost:9876"); //指定自己服务器的ip
//4.开启发送
producer.start();
// 异步发送失败的重试次数
producer.setRetryTimesWhenSendAsyncFailed(0);
//5.创建消息对象
Message message = new Message("topic1","发送的消息".getBytes(StandardCharsets.UTF_8));
//6.发送消息
int messageCount = 5;
final CountDownLatch2 downLatch2 = new CountDownLatch2(messageCount);
for (int i = 0; i < messageCount; i++) {
final int index = i;
producer.send(message, new SendCallback() {
//发送成功
@Override
public void onSuccess(SendResult sendResult) {
downLatch2.countDown();
System.out.printf("%-1d 发送成功 %s %n",index,sendResult.getMsgId());
}
// 出现异常
@Override
public void onException(Throwable throwable) {
downLatch2.countDown();
System.out.printf("%-1d Exception %s %n", index, throwable);
throwable.printStackTrace();
}
});
}
// 等待5s
downLatch2.await(5, TimeUnit.SECONDS);
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
🍀接收消息同步异步并无区别
- 单向消息:
public class OnewayProducer {
public static void main(String[] args) throws Exception{
// 实例化消息生产者Producer
DefaultMQProducer producer = new DefaultMQProducer("group_name");
// 设置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 启动Producer实例
producer.start();
for (int i = 0; i < 5; i++) {
// 创建消息,并指定Topic,Tag和消息体
Message msg = new Message("TopicTest","TagA",
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 发送单向消息,没有任何返回结果
producer.sendOneway(msg);
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
延时消息:
// 设置延时等级3,这个消息将在10s之后发送
message.setDelayTimeLevel(3);
// 发送消息
producer.send(message);
// 延时消息分为18个等级,如果level==0,则不延迟
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
批量发送消息:
- 批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic,相同的waitStoreMsgOK,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB。
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
producer.send(messages);
} catch (Exception e) {
e.printStackTrace();
//处理error
}
消息的过滤:
tag
标签过滤
//创建消息对象
Message message = new Message("topic1","tag","发送的消息".getBytes(StandardCharsets.UTF_8));
//发送消息
SendResult sendResult = producer.send(message,10000);
consumer.subscribe("topic1","tag || taga || tagb"); // 可以使用 ||符号
//4.注册一个监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}});
//.开启发送
consumer.start();
-
sql
过滤- 基本语法:
RocketMQ
只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。- 数值比较,比如:
>
,>=
,<
,<=
,BETWEEN
,=
; - 字符比较,比如:
=
,<>
,IN
; IS NULL
或者IS NOT NULL
;- 逻辑符号
AND
,OR
,NOT
;
常量支持类型为: - 数值,比如:123,3.1415;
- 字符,比如:
'abc'
,必须用单引号包裹起来; NULL
,特殊的常量- 布尔值,
TRUE
或FALSE
- 数值比较,比如:
🍀只有使用push模式的消费者才能用使用SQL92标准的sql语句,所以我们需要开启对sql的支持,修改conf/broker.conf文件
-
Producer
//创建消息对象
Message message = new Message("topic1","发送的消息".getBytes(StandardCharsets.UTF_8));
// 设置一些属性
msg.putUserProperty("a", "19");
consumer
//设置topic以及flag
consumer.subscribe("topic1", MessageSelector.bySql("a >= 18"));
顺序发送消息:
- 比如根据某个对象的id进行分类,选择不同的消息队列
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long id = (Long) arg; //根据订单id选择发送queue
long index = id % mqs.size();
return mqs.get((int) index);
}
}, orderList.get(i).getOrderId());//订单id
- 取队列中的消息需要变更原来的监听器
/**
* 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
* 如果非第一次启动,那么按照上次消费的位置继续消费
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.registerMessageListener(new MessageListenerOrderly() {
Random random = new Random();
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
for (MessageExt msg : msgs) {
// 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
}
try {
//模拟业务逻辑处理中...
TimeUnit.SECONDS.sleep(random.nextInt(10));
} catch (Exception e) {
e.printStackTrace();
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
🍀由于消费者在消费消息时,只能单线程消费消息那么效率毫无疑问肯定会下降,我想这一点是你需要知道的。
事物发送消息:
- 事物的状态:
- 🌻
TransactionStatus.CommitTransaction
: 提交事务,它允许消费者消费此消息。 - 🌻
TransactionStatus.RollbackTransaction
: 回滚事务,它代表该消息将被删除,不允许被消费。 - 🌻
TransactionStatus.Unknown
: 中间状态,它代表需要检查消息队列来确定状态。
- 🌻
Producer
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
producer.setExecutorService(executorService);
producer.setTransactionListener(transactionListener);
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 10; i++) {
try {
Message msg =
new Message("TopicTest1234", tags[i % tags.length], "KEY" + i,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
System.out.printf("%s%n", sendResult);
Thread.sleep(10);
} catch (MQClientException | UnsupportedEncodingException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 100000; i++) {
Thread.sleep(1000);
}
producer.shutdown();
}
}
Consumer
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
return LocalTransactionState.UNKNOW;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Integer status = localTrans.get(msg.getTransactionId());
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
-
事物的注意点:
- 事务消息不支持延时消息和批量消息。
- 为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionalMessageCheckListener 类来修改这个行为。
- 事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionTimeout 参数。
- 事务性消息可能不止一次被检查或消费。
- 提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
- 事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。