分布式消息系统
采用java开发的分布式消息系统,由阿里开发
地址:http://rocketmq.apache.org/
Producer
Consumer
消费者:负责消费消息,一般由后台系统负责异步消费
分类:
Consmer Group:一类Consumer的集合名称,这类Producer通常发送同一类消息,且发送逻辑一致
Broker
NameServer
Topic【逻辑概念】
下载地址:https://archive.apache.org/dist/rocketmq/4.3.2/rocketmq-all-4.3.2-bin-release.zip
cd /opt
unzip rocketmq-all-4.3.2-bin-release.zip
cd rocketmq-all-4.3.2-bin-release/
# 启动nameserver
bin/mqnamesrv
#The Name Server boot success. serializeType=JSON
# 看到这个说明nameserver启动成功
#启动broker
bin/mqbroker -n 8.140.130.91:9876 #-n指定nameserver地址和端口
Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000005c0000000, 8589934592, 0) failed; error='Cannot allocate memory' (errno=12)
启动错误,因为RocketMQ的配置默认是生产环境的配置,设置jvm的内存值比较大,需要调整默认值
#调整默认的内存大小参数
cd bin/
JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx128m -Xmn128m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m"
vim runbroker.sh
JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx128m -Xmn128m"
#重新启动测试
bin/mqbroker -n 8.140.130.91:9876
The broker[iZ2zeg4pktzjhp9h7wt6doZ, 172.17.0.1:10911] boot success. serializeType=JSON and name server is 8.140.130.91:9876#启动成功
发送消息测试:
export NAMESRV_ADDR=127.0.0.1:9876
cd /opt/rocketmq-all-4.3.2-bin-release/bin
sh tools.sh org.apache.rocketmq.example.quickstart.Producer
接收消息测试:
sh tools.sh org.apache.rocketmq.example.quickstart.Consumer
依赖
<dependencies>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.3.2version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
测试代码
package com.rocketmq;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.io.UnsupportedEncodingException;
public class SyncProducer {
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
DefaultMQProducer producer = new DefaultMQProducer("test-group");
//specify name server address
producer.setNamesrvAddr("8.140.130.91:9876");
//Lanuch the instance
producer.start();
for (int i = 0; i < 100; i++) {
//create message instance ,specify topic,tag and message body
Message msg =
new Message(
"TopicTest1",/*topic*/
"TAGA",/*tag*/
("Hello RocketMQ" + i).getBytes(
RemotingHelper.DEFAULT_CHARSET
)/*message body*/
);
//Call send message to deliver message to one of brokers.
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
}
发现报错
原因:
broker的ip地址是172.17.0.1,为私有ip,所以不可访问
解决:修改broker配置文件,指定broker 的ip地址
cd /opt/rocketmq-all-4.3.2-bin-release/conf
vim broker.conf
brokerIP1=8.140.130.91
namesrvAddr=8.140.130.91:9876
brokerName=broker_haoke_im
#启动broker,通过 -c 指定配置文件
cd /opt/rocketmq-all-4.3.2-bin-release/
bin/mqbroker -c /opt/rocketmq-all-4.3.2-bin-release/conf/broker.conf
API测试成功
#拉取镜像
docker pull foxiswho/rocketmq:server-4.3.2
docker pull foxiswho/rocketmq:broker-4.3.2
#创建nameserver容器
docker create -p 9876:9876 --name rmqserver \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-e "JAVA_OPTS=-Duser.home=/opt" \
-v /data/rmq-data/rmqserver/logs:/opt/logs \
-v /data/rmq-data/rmqserver/store:/opt/store \
foxiswho/rocketmq:server-4.3.2
#创建broker容器
#10911 生产者,消费者端口
#10909 搭建集群主从端口
docker create -p 10911:10911 -p 10909:10909 --name rmqbroker \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-v /data/rmq-data/rmqbroker/conf/broker.conf:/etc/rocketmq/broker.conf \
-v /data/rmq-data/rmqbroker/logs:/opt/logs \
-v /data/rmq-data/rmqbroker/store:/opt/store \
foxiswho/rocketmq:broker-4.3.2
#启动容器
docker start rmqserver rmqbroker
#停止删除容器
docker stop rmqbroker rmqserver
docker rm rmqbroker rmqserver
#broker名
brokerName=broker_haoke_im
#broker IP
brokerIP1=8.140.130.91
#当前broker托管的NameServer地址
namesrvAddr=8.140.130.91:9876
#开启自定义属性支持
enablePropertyFilter=true
UI管理工具,rocketmq-console,项目地址https://github.com/apache/rocketmq-externals/tree/master/rocketmq-console
#拉取镜像
docker pull apacherocketmq/rocketmq-console:2.0.0
#创建并启动容器
docker run -e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=8.140.130.91:9876 -Drocketmq.config.isVIPChannel=false" -p 8082:8080 -t apacherocketmq/rocketmq-console:2.0.0
访问:http://8.140.130.91:8082/
package com.rocketmq;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
public class TopicDemo {
public static void main(String[] args) throws Exception{
//设置NameServer地址
DefaultMQProducer producer = new DefaultMQProducer("test-group");
//设置producer 的NameServerAddress
producer.setNamesrvAddr("8.140.130.91:9876");
//启动NameServer
producer.start();
/*
* 创建topic
* @param key broker name
* @param newTopic topic name
* @param queueNum topic's queue number
* */
producer.createTopic("broker_haoke_im","test_topic",8);
System.out.println("topic创建成功");
producer.shutdown();
}
}
字段名 | 默认 值 | 说明 |
---|---|---|
Topic | null | 必填,线下环境不需要申请,线上环境需要申请后才能使用 |
Body | null | 必填,二进制形式,序列化由应用决定,Producer 与 Consumer 要协商好 序列化形式。 |
Tags | null | 选填,类似于 Gmail 为每封邮件设置的标签,方便服务器过滤使用。目前只 支持每个消息设置一个 tag,所以也可以类比为 Notify 的 MessageType 概 念 |
Keys | null | 选填,代表这条消息的业务关键词,服务器会根据 keys 创建哈希索引,设置 后,可以在 Console 系统根据 Topic、Keys 来查询消息,由于是哈希索引, 请尽可能保证 key 唯一,例如订单号,商品 Id 等。 |
Flag | 0 | 选填,完全由应用来设置,RocketMQ 不做干预 |
DelayTimeLevel | 0 | 选填,消息延时级别,0 表示不延时,大于 0 会延时特定的时间才会被消费 |
WaitStoreMsgOK | TRUE | 选填,表示消息是否在服务器落盘后才返回应答。 |
package com.rocketmq.message;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class SyncMessage {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("test-group");
producer.setNamesrvAddr("8.140.130.91:9876");
producer.start();
String msgStr = "测试消息1";
/*
* String topic, String tags, byte[] body
* */
Message message = new Message("test_topic","test",msgStr.getBytes("UTF-8"));
SendResult result = producer.send(message);
System.out.println(result);
System.out.println("消息状态:" + result.getSendStatus());
System.out.println("消息id:" + result.getMsgId());
System.out.println("消息queue:" + result.getMessageQueue());
System.out.println("消息offset:" + result.getQueueOffset());
producer.shutdown();
}
}
与同步区别在于,回调函数的执行是滞后的,主程序是顺序执行的
package com.rocketmq.message;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class AsyncMessage {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("test-group");
producer.setNamesrvAddr("8.140.130.91:9876");
producer.start();
String msgStr = "异步消息发送测试";
/*
* String topic, String tags, byte[] body
* */
Message message = new Message("test_topic","test",msgStr.getBytes("UTF-8"));
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult result) {
System.out.println(result);
System.out.println("消息状态:" + result.getSendStatus());
System.out.println("消息id:" + result.getMsgId());
System.out.println("消息queue:" + result.getMessageQueue());
System.out.println("消息offset:" + result.getQueueOffset());
}
@Override
public void onException(Throwable e) {
System.out.println("消息发送失败");
}
});
// producer.shutdown()要注释掉,否则发送失败。原因是,异步发送,还未来得及发送就被关闭了
//producer.shutdown();
}
}
package com.rocketmq.consumer;
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.io.UnsupportedEncodingException;
import java.util.List;
public class ConsumerDemo {
public static void main(String[] args) throws Exception{
/*
* push类型的消费者,被动接收从broker推送的消息
* */
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-group");
consumer.setNamesrvAddr("8.140.130.91:9876");
//订阅topic,接收此topic下的所有消息
consumer.subscribe("test_topic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {//并发读取消息
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
System.out.println(new String(msg.getBody(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
System.out.println("收到消息->"+msgs);
/*
* 返回给broker消费者的接收情况
* CONSUME_SUCCESS 接收成功
* RECONSUME_LATER 延时重发
* */
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
测试接收历史消息:
测试接收实时消息:
可以通过tag区分不同类型
#生产者
Message message = new Message("test_topic","add",msgStr.getBytes("UTF-8"));
#消费者
//完整匹配
consumer.subscribe("test_topic","add");
//或匹配
consumer.subscribe("test_topic","add || delete");
RocketMQ支持根据用户自定义属性进行过滤 ,类似与SQL
MessageSelector.bySql(“age>=20 AND sex=‘女’”));
消息发送方:
package com.rocketmq.filter;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
/**
* @author Auspice Tian
* @time 2021-04-04 15:10
* @current example-roketmq-com.rocketmq.filter
*/
public class SyncProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("test-group");
producer.setNamesrvAddr("8.140.130.91:9876");
producer.start();
String msgStr = "发送测试";
Message msg = new Message("test_topic","test",msgStr.getBytes("UTF-8"));
msg.putUserProperty("age","18");
msg.putUserProperty("sex","女");
SendResult result = producer.send(msg);
System.out.println("消息状态"+result.getSendStatus());
System.out.println("消息id"+ result.getMsgId());
System.out.println("消息queue"+result.getMessageQueue());
System.out.println("消息offset"+result.getQueueOffset());
producer.shutdown();
}
}
消息接收方:
package com.rocketmq.filter;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MessageSelector;
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.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class ConsumerFilter {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-group");
consumer.setNamesrvAddr("8.140.130.91:9876");
consumer.subscribe("test_topic", MessageSelector.bySql("age>=20 AND sex='女'"));
consumer.registerMessageListener(new MessageListenerConcurrently() {//并发读取消息
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
System.out.println(new String(msg.getBody(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
System.out.println("收到消息->"+msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
测试:
消息发送成功,但是由于不满足条件,被过滤器过滤,消费者未接收到
修改生产者自定义属性
Message msg = new Message("test_topic","test",msgStr.getBytes("UTF-8"));
msg.putUserProperty("age","21");
msg.putUserProperty("sex","女");
可以接收到消息
消息的顺序收发,需要消费者与生产者二者配合
package com.rocketmq.order;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class OrderProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("test-group");
producer.setNamesrvAddr("8.140.130.91:9876");
producer.start();
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
//生产10个订单的消息,每个订单10条消息
String msgStr = "order-->"+i + " orderId-->" + orderId;
Message message = new Message("test_topic","ORDER_MSG",msgStr.getBytes("UTF-8"));
/*
* public SendResult send(Message msg, MessageQueueSelector selector, Object arg)
* MessageQueue select(final List mqs, final Message msg, final Object arg);
* */
SendResult sendResult = producer.send(
message,
(mqs,msg,arg)->{//匿名函数的作用为选择消息队列的id
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
},//arg与orderId对应,
orderId);
System.out.println(sendResult);
}
producer.shutdown();
}
}
public class OrderConsumer {
public static void main(String[] args) throws Exception{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-order-group");
consumer.setNamesrvAddr("8.140.130.91:9876");
consumer.subscribe("test_order_topic","*");
consumer.registerMessageListener(new MessageListenerOrderly() {//顺序读取消息
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
try {
System.out.println(
Thread.currentThread()
.getName() + " "
+ msg.getQueueId() + " "
+ new String(msg.getBody(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
}
}
可见,订单id为3的消息,会存入同一消息队列,故在同一消息队列的消息可被同一消费线程监听
分布式事务分类:
Half(Prepare) Message
消息系统暂时不能投递的消息:发送方将消息发送到了MQ服务端。MQ服务端未收到生产者对消息的二次确认,此时该消息被标记为 暂不能投递状态 处于该状态的消息称为 半消息
Message Status Check
由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,MQ服务端发现某条消息长期处于 半消息,需要主动向消息生产者询问该消息的状态
发送方向MQ服务端发送消息
MQ Server将消息持久化成功后,向发送方ACK确认消息已经发送成功,此时消息为 半消息
发送方开始执行本地事务逻辑
发送方根据本地事务执行结果向MQ Server提交二次确认(Commit或Rollback),MQ Server 收到 Commit 则将半消息标记为 可投递,订阅方最终收到该消息;MQ Server收到 Rollback ,则删除该半消息,订阅方不会收到该消息
在断网或应用重启情况下,上述4提交的二次确认最终未到达MQ Server,经过固定时间后,MQ Server将对该消息发起消息回查
发送方收到消息回查,需要检查对应消息的本地事务执行的最终结果
发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server仍按4对半消息进行确认
package com.rocketmq.trancation;
public class TrancationProducer {
public static void main(String[] args) throws Exception{
TransactionMQProducer producer = new TransactionMQProducer("test_transaction_producer");
producer.setNamesrvAddr("8.140.130.91:9876");
//设置事务监听器
producer.setTransactionListener(new TransactionImpl());
producer.start();
//发送消息
Message message = new Message("pay_topic","用户A给用户B转钱".getBytes("UTF-8"));
producer.sendMessageInTransaction(message,null);
Thread.sleep(99999);
producer.shutdown();
}
}
package com.rocketmq.trancation;
public class TransactionImpl implements TransactionListener {
private static Map<String, LocalTransactionState> STATE_MAP = new HashMap<>();
/**
* 本地执行业务具体的逻辑
* @param msg
* @param arg
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Thread.sleep(500);
System.out.println("用户A账户减500");
// System.out.println(1/0);
System.out.println("用户B账户加500元.");
Thread.sleep(800);
//二次提交确认
STATE_MAP.put(msg.getTransactionId(),LocalTransactionState.COMMIT_MESSAGE);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (InterruptedException e) {
e.printStackTrace();
}
//回滚
STATE_MAP.put(msg.getTransactionId(), LocalTransactionState.ROLLBACK_MESSAGE);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
/**
* 消息回查
* @param msg
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
return STATE_MAP.get(msg.getTransactionId());
}
}
package com.rocketmq.trancation;
public class TransactionConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_transaction_consumer");
consumer.setNamesrvAddr("8.140.130.91:9876");
//订阅topic,接收消息
consumer.subscribe("pay_topic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
System.out.println(new String(msg.getBody(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
返回 commit 状态时,消费者能够接收消息
返回 rollback 状态时,消费者接收不到消息
消息回查测试
public class TransactionImpl implements TransactionListener {
private static Map<String, LocalTransactionState> STATE_MAP = new HashMap<>();
/**
* 本地执行业务具体的逻辑
* @param msg
* @param arg
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
System.out.println("用户A账户减500");
Thread.sleep(500);
// System.out.println(1/0);
System.out.println("用户B账户加500元.");
Thread.sleep(800);
//二次提交确认
STATE_MAP.put(msg.getTransactionId(),LocalTransactionState.COMMIT_MESSAGE);
return LocalTransactionState.UNKNOW;
// return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
e.printStackTrace();
}
//回滚
STATE_MAP.put(msg.getTransactionId(), LocalTransactionState.ROLLBACK_MESSAGE);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
/**
* 消息回查
* @param msg
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("状态回查-->"+ msg.getTransactionId() + " "+ STATE_MAP.get(msg.getTransactionId()));
return STATE_MAP.get(msg.getTransactionId());
}
}
push模式需要消息系统与消费端之间建立长连接,对消息系统是很大的负担,所以在具体实现时,都采用消费端主动拉取的方式,即consumer轮询从broker拉取消息
在RocketMQ中,push与pull的区别
Push:
DefaultPushConsumer
将轮询过程都封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener监听器的consumeMessage()来消费,对用户而言,感觉消息是被推送来的。Pull:取消息过程需要自己写:首先从目标topic中拿到MessageQueue集合并遍历,然后针对每个MessageQueue批量取消息。一次Pull,都要记录该队列的offset,知道去完MessageQueue,再换另一个
长轮询(长连接+轮询),客户端像传统轮询一样从服务端请求数据,服务端会阻塞请求不会立刻返回,直到有数据或超时才返回给客户端,然后关闭连接,客户端处理完响应信息后再向服务器发送新的请求
DefaultMQPushConsumer实现了自动保存offset值及多个consumer的负载均衡
//设置组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("HAOKE_IM");
通过 groupname
将多个consumer组合在一起,会存在消息的分配问题(消息是发送到组还是每个消费者)
集群模式(默认)
同一个ConsumerGroup里的每个Consumer只消费所订阅消息的一部分内容,同一个ConsumerGroup里所有消费的内容合起来才是所订阅Topic内容的整体,从而达到负载均衡的目的
广播模式
同一个ConsumerGroup里的每个Consumer都能消费到所订阅Topic的全部消息,一个消息会被分发多次,被多个Consumer消费
// 集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
// 广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
重复消息的产生情况:
生产者不断发送重复消息到消息系统
网络不可达 :只要通过网络交换数据,就无法避免这个问题
由于接收到重复消息不可避免,问题变为 消费端收到重复消息,怎么处理
消费端处理消息的业务逻辑保持幂等性
幂等性:无论执行多少次,结果都一样
eg:while s!=1;在执行sql语句
保证每条消息都有唯一编号且保证消息处理成功与去重的日志同时出现
利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息
如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是 RocketMQ不解决消息重复的问题 的原因
RocketMQ中的消息数据存储,采用了零拷贝技术(mmap + write方式),文件系统采用 Linux Ext4文件系统进行存储。
在RocketMQ中,消息数据是保存在磁盘文件中的,使用RocketMQ尽可能保证顺序写入,比随机写入效率高很多
ConsumeQueue:索引文件,存储数据指向物理文件的位置
CommitLog是真正存储数据的文件
消息主体及元数据都存储在CommitLog中
Consume Queue 是一个逻辑队列,存储了这个Queue在CommitLog中的其实offset、log大小和MessageTag的hashcode
每次读取消息队列先读取ConsumerQueue,然后再通过consumerQueue中拿到消息主体
RocketMQ为提高性能,会尽可能保证磁盘的顺序读写。消息通过Producer写入RocketMQ的时候,有两种写磁盘方式,分别是同步刷盘与异步刷盘
修改刷盘方式
broker.conf
flushDiskType=ASYNC_FLUSH——异步
flushDiskType=SYNC_FLUSH——同步
在消息的发送和消费过程中,都有可能出现错误,如网络异常等,出现了错误就需要进行错误重试,这种消息的重试分为 producer端的重试 和 consumer端重试
//消息发送失败时,重试3次
producer.setRetryTimesWhenSendFailed(3);
// 发送消息,并且指定超时时间
SendResult sendResult = producer.send(msg, 1000);
#DefaultMQProducerImpl
//设置发送总次数
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
for (; times < timesTotal; times++) {
try{
if (timeout < costTime) {
callTimeout = true;
break;
}
}catch (RemotingException e) {
...
continue;
}catch (MQClientException e) {
...
continue;
}catch (MQBrokerException e){
switch (e.getResponseCode()) {
case ResponseCode.TOPIC_NOT_EXIST:
case ResponseCode.SERVICE_NOT_AVAILABLE:
case ResponseCode.SYSTEM_ERROR:
case ResponseCode.NO_PERMISSION:
case ResponseCode.NO_BUYER_ID:
case ResponseCode.NOT_IN_CURRENT_UNIT:
continue;
}
}
}
消息正常到了消费者端,处理失败,发生异常。eg:反序列化失败,消息数据本身无法处理
消息状态
package org.apache.rocketmq.client.consumer.listener;
public enum ConsumeConcurrentlyStatus {
/**
* Success consumption
*/
CONSUME_SUCCESS,
/**
* Failure consumption,later try to consume
*/
RECONSUME_LATER;
}
broker的启动日志
INFO main - messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
如果消息消费失败即broker收到 RECONSUME_LATER ,则broker会对消息进行重试发送,直至2h
演示:
public class ConsumerDemo {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_consumer_group");
consumer.setNamesrvAddr("8.140.130.91:9876");
// 订阅topic,接收此Topic下的所有消息
consumer.subscribe("test_error_topic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
System.out.println(new String(msg.getBody(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
System.out.println("收到消息->" + msgs);
if(msgs.get(0).getReconsumeTimes() >= 3){
// 重试3次后,不再进行重试
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
}
}
重试消息和原始发送消息不是同一条
由于消息没有从MQ发送到消费者上,那么在MQ Server内部会不断的尝试发送这条消息,直至发送成功位置
也就是,服务端没有接收到消费端发来的消息的反馈,定义为超时
单个Master
多Master
多Master多Slave,异步复制
多Master多Slave,同步双写
创建2个NameServer(master)
#nameserver1
docker create -p 9876:9876 --name rmqserver01 \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-e "JAVA_OPTS=-Duser.home=/opt" \
-v /data/rmq-data/rmqserver01/logs:/opt/logs \
-v /data/rmq-data/rmqserver01/store:/opt/store \
foxiswho/rocketmq:server-4.3.2
#nameserver2
docker create -p 9877:9876 --name rmqserver02 \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-e "JAVA_OPTS=-Duser.home=/opt" \
-v /data/rmq-data/rmqserver02/logs:/opt/logs \
-v /data/rmq-data/rmqserver02/store:/opt/store \
foxiswho/rocketmq:server-4.3.2
搭建broker(2master)
#broker01配置文件
namesrvAddr=8.140.130.91:9876;8.140.130.91:9877
brokerClusterName=HaokeCluster
brokerName=broker01
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=SYNC_MASTER
flushDiskType=ASYNC_FLUSH
brokerIP1=8.140.130.91
brokerIp2=8.140.130.91
listenPort=11911
#master broker01
docker create --net host --name rmqbroker01 \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-v /data/rmq-data/rmqbroker01/conf/broker.conf:/etc/rocketmq/broker.conf \
-v /data/rmq-data/rmqbroker01/logs:/opt/logs \
-v /data/rmq-data/rmqbroker01/store:/opt/store \
foxiswho/rocketmq:broker-4.3.2
brokerId:0表示主,>0表示Slave
fileReservedTime:消息保存时间 单位——h
deleteWhen:什么是时候对过期消息清理 24小时制
brokerRole:[同步双写|异步双写]_[主] | [从]
[SYNC|ASYNC_MASTER] | [SLAVE]
flushDiskType:刷盘方式 [同步|异步]_FLUSH
[SYNC|ASYNC_FLUSH]
brokerIP1:访问broker的ip地址
brokerIP2:主从同步的ip
listenPort:与客户端交互的端口(+1,-2)
#broker02配置文件
namesrvAddr=8.140.130.91:9876;8.140.130.91:9877
brokerClusterName=HaokeCluster
brokerName=broker02
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=SYNC_MASTER
flushDiskType=ASYNC_FLUSH
brokerIP1=8.140.130.91
brokerIp2=8.140.130.91
listenPort=11811
#master broker02
docker create --net host --name rmqbroker02 \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-v /data/rmq-data/rmqbroker02/conf/broker.conf:/etc/rocketmq/broker.conf \
-v /data/rmq-data/rmqbroker02/logs:/opt/logs \
-v /data/rmq-data/rmqbroker02/store:/opt/store \
foxiswho/rocketmq:broker-4.3.2
搭建从broker(slave)
#slave broker01配置文件
namesrvAddr=8.140.130.91:9876;8.140.130.91:9877
brokerClusterName=HaokeCluster
brokerName=broker01
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH
brokerIP1=8.140.130.91
brokerIp2=8.140.130.91
listenPort=11711
#slave broker01
docker create --net host --name rmqbroker03 \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-v /data/rmq-data/rmqbroker03/conf/broker.conf:/etc/rocketmq/broker.conf \
-v /data/rmq-data/rmqbroker03/logs:/opt/logs \
-v /data/rmq-data/rmqbroker03/store:/opt/store \
foxiswho/rocketmq:broker-4.3.2
#slave broker02配置文件
namesrvAddr=8.140.130.91:9876;8.140.130.91:9877
brokerClusterName=HaokeCluster
brokerName=broker02
brokerId=1
deleteWhen=04
fileReservedTime=48
brokerRole=SLAVE
flushDiskType=ASYNC_FLUSH
brokerIP1=8.140.130.91
brokerIp2=8.140.130.91
listenPort=11611
#slave broker02
docker create --net host --name rmqbroker04 \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms128m -Xmx128m -Xmn128m" \
-v /data/rmq-data/rmqbroker04/conf/broker.conf:/etc/rocketmq/broker.conf \
-v /data/rmq-data/rmqbroker04/logs:/opt/logs \
-v /data/rmq-data/rmqbroker04/store:/opt/store \
foxiswho/rocketmq:broker-4.3.2
#启动容器
docker start rmqserver01 rmqserver02
docker start rmqbroker01 rmqbroker02 rmqbroker03 rmqbroker04
生产者
public class SyncMessage {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("test_cluster_group");
producer.setNamesrvAddr("8.140.130.91:9876;8.140.130.91:9877");
producer.start();
String msgStr = "Cluster测试消息";
/*
* String topic, String tags, byte[] body
* */
Message message = new Message("test_cluster_topic","CLUSTER",msgStr.getBytes("UTF-8"));
SendResult result = producer.send(message);
System.out.println(result);
System.out.println("消息状态:" + result.getSendStatus());
System.out.println("消息id:" + result.getMsgId());
System.out.println("消息queue:" + result.getMessageQueue());
System.out.println("消息offset:" + result.getQueueOffset());
producer.shutdown();
}
}
消费者
public class ConsumerDemo {
public static void main(String[] args) throws Exception{
/*
* push类型的消费者,被动接收从broker推送的消息
* */
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_cluster_group");
consumer.setNamesrvAddr("8.140.130.91:9876;8.140.130.91:9877");
//订阅topopic,接收此topic下的所有消息
consumer.subscribe("test_cluster_topic","*");
consumer.registerMessageListener(new MessageListenerConcurrently() {//并发读取消息
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
System.out.println(new String(msg.getBody(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
System.out.println("收到消息->"+msgs);
/*
* 返回给broker消费者的接收情况
* CONSUME_SUCCESS 接收成功
* RECONSUME_LATER 延时重发
* */
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
由于rocketMQ没有发布到Mven中央仓库,需要自行下载源码,并载入到本地Maven仓库
#源码地址
https://hub.fastgit.org/apache/rocketmq-spring
#进入源码目录,执行
mvn clean install
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.3version>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.3.2version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
#Spring boot application
spring.application.name = test-rocketmq
spring.rocketmq.nameServer=8.140.130.91:9876
spring.rocketmq.producer.group=test_spring_producer_group
package com.rocketmq.spring;
@Component
public class SpringProducer {
//注入rocketmq模板
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送消息
*
* @param topic
* @param msg
*/
public void sendMsg(String topic,String msg){
this.rocketMQTemplate.convertAndSend(topic,msg);
}
}
package com.rocketmq;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class,args);
}
}
package com.rocketmq.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestSpringRocketMQ {
@Autowired
SpringProducer producer;
@Test
public void testSendMsg(){
String msg = "第二个Spring RocketMq 消息";
this.producer.sendMsg("test_spring_topic",msg);
System.out.println("发送成功!");
}
}
package com.rocketmq.spring;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = "test_spring_topic",
consumerGroup = "test_spring_consumer_group",
selectorExpression = "*",
consumeMode = ConsumeMode.CONCURRENTLY
)
public class SpringConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String msg) {
System.out.println("收到消息->"+msg);
}
}
package com.rocketmq.spring.transaction;
@Component
public class TransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送消息
*
* @param topic
* @param msg
*/
public void sendMsg(String topic,String msg){
Message message = (Message) MessageBuilder.withPayload(msg).build();
//此处的txProducerGroup与事务监听器的@RocketMQTransactionListener(txProducerGroup = "")一致
this.rocketMQTemplate.sendMessageInTransaction(
"test_tx_producer_group",
topic,
message,
null
);
System.out.println("消息发送成功");
}
}
package com.rocketmq.spring.transaction;
@RocketMQTransactionListener(txProducerGroup = "test_tx_producer_group")
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
private static Map<String,RocketMQLocalTransactionState> STATE_MAP = new HashMap<>();
/**
* 执行本地事务
*
* @param message
* @param o
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
String transactionId = (String) message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
try {
System.out.println("执行操作1");
Thread.sleep(500L);
System.out.println("执行操作2");
Thread.sleep(500L);
STATE_MAP.put(transactionId,RocketMQLocalTransactionState.COMMIT);
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
e.printStackTrace();
}
STATE_MAP.put(transactionId,RocketMQLocalTransactionState.ROLLBACK);
return RocketMQLocalTransactionState.ROLLBACK;
}
/**
* 消息回查
*
* @param message
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
String transactionId = (String) message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
System.out.println("回查消息->transactionId = "+transactionId+",state = "+STATE_MAP.get(transactionId));
return STATE_MAP.get(transactionId);
}
}
@Test
public void testSendTransactionMsg(){
String msg = "事务消息测试!";
this.transactionProducer.sendMsg("test_spring_transaction_topic",msg);
System.out.println("发送成功");
}
package com.rocketmq.spring.transaction;
@Component
@RocketMQMessageListener(
topic = "test_spring_transaction_topic",
consumeMode = ConsumeMode.CONCURRENTLY,
selectorExpression = "*",
consumerGroup = "test_tx_consumer_group"
)
public class TransactionConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String s) {
System.out.println("收到消息->"+s);
}
}
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
String transactionId = (String) message.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
try {
System.out.println("执行操作1");
Thread.sleep(500L);
System.out.println("执行操作2");
Thread.sleep(500L);
STATE_MAP.put(transactionId,RocketMQLocalTransactionState.COMMIT);
return RocketMQLocalTransactionState.UNKNOWN;
}catch (Exception e){
e.printStackTrace();
}
STATE_MAP.put(transactionId,RocketMQLocalTransactionState.ROLLBACK);
return RocketMQLocalTransactionState.ROLLBACK;
}