单体服务
下载当前最新版本的rocketmq:Release Notes - Apache RocketMQ - Version 4.9.3 - Apache RocketMQ
rocketmq是java实现的,由于它的高并发而且低延迟的特性,所需要的内存很大,解压后需要修改runserver.sh和runbroker.sh配置文件的堆大小以及元数据空间大小,将默认的java heap配置根据自己虚机的资源进行调整。我的机器实在太烂,堆内存设置比较小
runserver.sh
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx512m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
runbroker.sh
JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m"
修改conf目录的broker.conf配置自动创建topic:
autoCreateTopicEnable=true
另外需要配置nameserver环境变量
export NAMESRV_ADDR='node01:9876;node02:9876;node03:9876'
cd到rocketmq的目录先后启动nameserver和rocketmq
nohup bin/mqnamesrv &
nohup bin/mqbroker &
测试消息:
bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
集群搭建
从conf目录可以看到,rocketmq支持异步和同步的2主2从,2主无从以及dledger集群,这里的异步指的是异步刷盘,吞吐量比较大,我们就搭建一个异步的2主2从的。使用两台机器node01和node03,分别部署broker-a、broker-b-s和broker-b、broker-a-s,以及两个nameserver。
配置文件
node01:broker-a.properties
#所属集群名字,名字一样的节点就在同一个集群内
brokerClusterName=rocketmq-cluster
#broker名字,名字一样的节点就是一组主从节点。
brokerName=broker-a
#brokerid,0就表示是Master,>0的都是表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=node01:9876;node03:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=10911
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 48 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径
storePathRootDir=/usr/local/data/rocketmq/master
#commitLog 存储路径
storePathCommitLog=/usr/local/data/rocketmq/master/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/usr/local/data/rocketmq/master/consumequeue
#消息索引存储路径
storePathIndex=/usr/local/data/rocketmq/master/index
#checkpoint 文件存储路径
storeCheckpoint=/usr/local/data/rocketmq/checkpoint
#abort 文件存储路径
abortFile=/usr/local/data/rocketmq/abort
#限制的消息大小
maxMessageSize=65536
#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#发消息线程池数量
#sendMessageThreadPoolNums=128
#拉消息线程池数量
#pullMessageThreadPoolNums=128
#开启过滤功能tag
enablePropertyFilter=true
由于配置比较长,所以就不粘贴其他的配置了,但是有几点需要注意
1、集群名称必须一样
2、broker名称,同一主从必须相同
3、brokerId:master配置为0,从节点配置大于0
4、消息存储目录:同一主机的broker必须不同,否则会启动失败
启动集群
node01:
nohup bin/mqnamesrv &
nohup bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
nohup bin/mqbroker -c conf/2m-2s-async/broker-b-s.properties &
node03:
nohup bin/mqnamesrv &
nohup bin/mqbroker -c conf/2m-2s-async/broker-b.properties &
nohup bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
注意要带-c选项,我在这里栽了。
同样可以通过以下命令进行测试集群是否搭建成功
bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
理解rocketmq主从工作机制
nameserver:服务注册与发现,broker会将元数据注册到nameserver,consumer和producer会从nameserver中获取broker信息。
producer:消息发送者,与kafka不同的是,这里有生产者组的概念:同一组的Producer被认为是发送同一类消息且发送逻辑一致。
broker:rocketmq服务实例
topic:同kafka的topic概念类似,也是一个逻辑概念,划分不同的业务,但是在rocketmq中,建议在一个服务实例中使用一个topic,将消息配置为不同的tag来划分不同类型的业务。
messageQueue:跟kafka的partition概念类似,真正用来存储消息的。默认为一个topic创建4个messageQueue,当然可以通过配置文件进行配置。
consumer:消费者,同kafka一样也有消费者组的概念。
使用案例
简单示例
1、同步发送消息
producer:
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
DefaultMQProducer producer = new DefaultMQProducer("test_group");
// Specify name server addresses.
producer.setNamesrvAddr("node01:9876");
//Launch the instance.
producer.start();
for (int i = 0; i < 100; i++) {
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTest" ,"TagA" ,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
//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();
}
rocketmq开发生产者固定步骤:
1、创建producer对象,并设置nameserver地址。
2、启动producer
3、创建消息体对象,并设置topic以及tag
4、发送消息:在发送消息后,线程会
5、关闭producer
consumer
public static void main(String[] args) throws InterruptedException, MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// Specify name server addresses.
consumer.setNamesrvAddr("node01:9876");
// Subscribe one more more topics to consume.
consumer.subscribe("TopicTest", "*");
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//Launch the consumer instance.
consumer.start();
System.out.printf("Consumer Started.%n");
}
2、异步发送消息
final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
countDownLatch.countDown();
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});
countDownLatch.await(5, TimeUnit.SECONDS);
这段代码摘自官方示例,有意思的是,它使用AQS实现CountDownLatch控制消息发送,主线程调用await()把自己阻塞挂起,等待消息发送完成后,rocketmq内部线程池会调用回调方法(onSuccess和onException)执行countDown()方法,最后唤醒主线程调用producer.shutdown()
3、onewaysend
这种方式可以理解为UTP协议,将消息发送出去以后,就不管了:producer.sendOneway(msg);
顺序消费
顺序消费消息可以理解为,消费者消费消息的顺序按照生产者生产消息的顺序来进行消费。从这个层面看,消息在broker中可以是没有顺序的,这样一来,就需要消费者消费消息的顺序就需要业务程序员来实现。但是在rocketmq中,消息从生产者到broker再到consumer都保证消息是有序的。
生产者:将一组需要顺序消费的消息有序的发送到同一个消息队列(MessageQueue)中,就能保证生产者和broker的消息有序。
示例代码:(为了能看清消息消费顺序,将官方示例producer的消息体略微修改,consumer端也略微修改)
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
Message msg =
new Message("OrderedTopic", tags[i % tags.length], "KEY" + i,
("orderId: " + orderId +", Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.printf("%s%n", sendResult);
}
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
context.setAutoCommit(true);
msgs.forEach(msg->{
System.out.println("Receive New Messages: "+ new String(msg.getBody()));
});
return ConsumeOrderlyStatus.SUCCESS;
}
});
结果分析:在producer日志中找到所有orderId为0的消息,它们同一被发送到了broker-a的queueId=0的队列中,保证了一组(orderId=0)的消息存储在一个队列中。
在消费者端找到所有orderId为0的消息,可以看到orderId=0的消息是有序消费的。
总结:rocketmq通过send(Message msg, MessageQueueSelector selector, Object arg)的第三个参数保证这一组的消息在消费时是有序的。从宏观角度看,rocketmq保证消息局部有序
延时消费
rocketmq提供了延时消费功能,像rabbitmq和kafka是不支持的。通过给消息体setDelayTimeLevel()方法设置延时级别就可以实现消息延时消费(3对应10s。。。)
//messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(3);
过滤消息
rocketmq通过tag标签实现消息的过滤,同时也支持sql。
广播消费
rocketmq在消费消息时,通过consumer.setMessageModel(MessageModel.BROADCASTING);设置consumer的消费模式,就可以将消费者设置为广播消费模式。区别于普通消费模式的是,每个消费者都会消费同一个消息,而不管消费者是否属于同一个消费者组。
批量消费
批量消费是指将多个消息合并为一个批量消息发送到broker,这样可以减少网络IO,但是批量消息也不能过大,否则会反而会降低消息的发送速率,rocketmq建议批量消息最大不超过1M,但实际使用时最大的限制是4M。而批量消息的使用是有限制的,这些消息有相同的topic和waitStoreMsgOK,而不能是延迟消息和事务消息
事务消息
rocketmq对事务消息的定义是两阶段提交消息保证消息本地事务执行和发送到broker同时成功或者失败,实现最终一致性。
对于事务消息官方还有一些说明,大致总结为以下几点:
1、事务消息不支持延迟消息和批量消息
2、通过参数可以修改本地事务回查的次数(transactionCheckMax)和回查的时间间隔。默认次数15次,当到达15次时系统会抛出异常。通过测试默认回查时间间隔为60s
官方示例(略微修改)
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.setNamesrvAddr("node01:9876;node03:9876");
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();
}
生产者创建事件监听器的实现类和线程池(执行状态回查),将他们设置到producer中,启动producer并发送消息。
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
/**
* 在提交完事务消息后执行;
* 返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
* 返回ROLLBACK_MESSAGE状态的消息会被丢弃。
* 返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
*
* @param msg Half(prepare) message
* @param arg Custom business parameter
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
System.out.println("executeLocalTransaction msg" + new String(msg.getBody()));
return LocalTransactionState.UNKNOW;
}
/**
* 在对UNKNOWN状态的消息会一直进行回查。
* @param msg Check message
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Integer status = localTrans.get(msg.getTransactionId());
System.out.println("checkLocalTransaction msg: " + new String(msg.getBody()) + ", status: " + status +", " +
"time: "+System.currentTimeMillis());
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
default:
return LocalTransactionState.COMMIT_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
TransactionStatus.CommitTransaction:允许消息被消费者消费
TransactionStatus.RollbackTransaction: 消息将会被删除并且不能被消费者消费
TransactionStatus.Unknown: MQ需要执行回查checkLocalTransaction()方法
执行结果分析:main线程每发送一条消息到broker(此时消息不能被消费)就会执行TransactionListenerImpl#executeLocalTransaction(),并返回unknow状态的消息。到check时间间隔后所有unknow状态的消息就会回调checkLocalTransaction()方法执行回查确定消息的状态。注意执行该方法的线程是程序员设置的线程池,是异步回调的。只有返回commit的消息才确认会被消费,返回rollback的要被删除掉,返回unknow状态会间隔一段时间后会继续回查。
小结
这里只演示了rocketmq的部分功能,还有一些功能没有讲述,比如trace,logappender等,可自行到官网查看。rocketmq为我们提供了很强大的功能,同时也提供了其他消息中间件没有的功能,并且经受了阿里双十一巨大压力的考验,推荐大家多学学