目录
7、RocketMQ生产者之MessageQueueSelector
1、消息队列RocketMQ4.X核心配置
生产者常见核心配置
compressMsgBodyOverHowmuch :消息超过默认字节4096后进行压缩
retryTimesWhenSendFailed : 失败重发次数
maxMessageSize : 最大消息配置,默认128k
topicQueueNums : 主题下面的队列数量,默认是4
autoCreateTopicEnable : 是否自动创建主题Topic, 开发建议为true,生产要为false
defaultTopicQueueNums : 自动创建服务器不存在的topic,默认创建的队列数
autoCreateSubscriptionGroup: 是否允许 Broker 自动创建订阅组,建议线下开发开启,线上关闭
brokerClusterName : 集群名称
brokerId : 0表示Master主节点 大于0表示从节点
brokerIP1 : Broker服务地址
brokerRole : broker角色 ASYNC_MASTER/ SYNC_MASTER/ SLAVE
deleteWhen : 每天执行删除过期文件的时间,默认每天凌晨4点
flushDiskType :刷盘策略, 默认为 ASYNC_FLUSH(异步刷盘), 另外是SYNC_FLUSH(同步刷盘)
listenPort : Broker监听的端口号
mapedFileSizeCommitLog : 单个conmmitlog文件大小,默认是1GB
mapedFileSizeConsumeQueue:ConsumeQueue每个文件默认存30W条,可以根据项目调整
storePathRootDir : 存储消息以及一些配置信息的根目录 默认为用户的 ${HOME}/store
storePathCommitLog:commitlog存储目录默认为${storePathRootDir}/commitlog
storePathIndex: 消息索引存储路径
syncFlushTimeout : 同步刷盘超时时间
diskMaxUsedSpaceRatio : 检测可用的磁盘空间大小,超过后会写入报错
2、RocketMQ4.X消息发送状态
消息发送有同步和异步
Broker消息投递状态讲解
FLUSH_DISK_TIMEOUT
没有在规定时间内完成刷盘 (刷盘策略需要为SYNC_FLUSH 才会出这个错误)
FLUSH_SLAVE_TIMEOUT
主从模式下,broker是SYNC_MASTER, 没有在规定时间内完成主从同步
SLAVE_NOT_AVAILABLE
从模式下,broker是SYNC_MASTER, 但是没有找到被配置成Slave的Broker
SEND_OK
发送成功,没有发生上面的三种问题
3、RocketMQ4.X生产和消费消息重试及处理
生产者Producer重试(异步和SendOneWay下配置无效)
消息重投(保证数据的高可靠性),本身内部支持重试,默认次数是2,
如果网络情况比较差,或者跨集群则建改多几次(集群跨国)
消费端重试
原因:消息处理异常、broker端到consumer端各种问题,如网络原因闪断,消费处理失败,ACK返回失败(消费成功后的返回)等等问题。
注意:
重试间隔时间配置 ,默认每条消息最多重试 16 次
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
超过重试次数人工补偿
消费端去重(全局表实现)
一条消息无论重试多少次,这些重试消息的 Message ID,key 不会改变。
消费重试只针对集群消费方式生效;
广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息,
模拟异常的情况
broker不会重试
消费者的模式一般都是不改变的,不能由于消息重试而改变消费模式
4、RocketMQ4.X异步发送消息和回调
官方文档:https://rocketmq.apache.org/docs/simple-example/
核心
producer.send(message, new SendCallback(){
onSuccess(){}
onException(){}
})
public class AsyncProducer {
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// Specify name server addresses.
producer.setNamesrvAddr("localhost:9876");
//Launch the instance.
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
for (int i = 0; i < 100; i++) {
final int index = i;
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d OK %s %n", index,
sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
}
注意:
官方例子:如果异步发送消息,调用producer.shutdown()后会失败(异步还没成功就把producer关掉了)
异步发送:不会重试,发送总次数等于1
5、RocketMQ OneWay发送消息及多种场景对比
SYNC :
应用场景:重要通知邮件、报名短信通知、营销短信系统等
ASYNC :异步
应用场景:对RT时间敏感,可以支持更高的并发,回调成功触发相对应的业务,
比如注册成功后通知积分系统发放优惠券(回调中做对应的业务逻辑)
ONEWAY : 无需要等待响应
官方文档:https://rocketmq.apache.org/docs/simple-example/
不管成不成功发过去就好了(可靠性要求不是很高)
public class OnewayProducer {
public static void main(String[] args) throws Exception{
//Instantiate with a producer group name.
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// Specify name server addresses.
producer.setNamesrvAddr("localhost: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" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
//Call send message to deliver message to one of brokers.
producer.sendOneway(msg);
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
}
使用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景, 也就是LogServer, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答
例如淘宝中,我们每点击一个东西,点击收藏等 都会有一个记录,记录后会提交到淘宝的服务进行分析
(日志回传LogServer)双11就会用onway队列少量机器采集大量的消息,不用部署太多的机器
一般现在都有日志记录
6、RocketMQ延迟消息实战和电商系统中应用
什么是延迟消息:
Producer 将消息发送到消息队列 RocketMQ 服务端,(传到broker)但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息,目前支持固定精度的消息
代码:rocketmq-store > MessageStoreConfig.java 属性 messageDelayLevel
"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
只能改时间,大部分的使用场景是足够使用的,一般不用更改
使用message.setDelayTimeLevel(xxx) //xxx是级别,1表示配置里面的第一个级别,2表示第二个级别
定时消息:目前rocketmq开源版本还不支持,商业版本则有,两者使用场景类似
使用场景
通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息(微信 QQ生日提醒)
前一天有个job轮询将明天生日的客户放到一个队列,第二天进行延时发送
消息生产和消费有时间窗口要求:比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。
这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略
7、RocketMQ生产者之MessageQueueSelector
生产消息使用MessageQueueSelector投递到Topic下指定的queue
应用场景:顺序消息,分摊负载
同步的方式
arg是与具体的业务强耦合的,根据不同的需求投递
默认topic下的queue数量是4,可以配置 broker.config
//可以使用Jdk8的lambda表达式,只有一个方法需要被实现
producer.send(message, new MessageQueueSelector(){
select(List<MessageQueue> mqs, Message msg, Object arg){
Integer queueNum = (Integer)arg;
return mqs.get(queueNum);
}
},0)
异步方式
lambda
支持同步,异步 发送指定的MessageQueue
选择的queue数量必须小于配置的,否则会出错
8、顺序消息在电商和证券系统中应用场景
顺序消息和对应可以使用的场景,订单系统
什么是顺序消息:消息的生产和消费顺序一致
全局顺序:topic下面全部消息都要有序(少用)改成一个queue
(1)性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景,并行度成为消息系统的瓶颈, 吞吐量不够.
(2)在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO 的方式进行发布和消费
局部顺序:只要保证一组消息被顺序消费即可(RocketMQ使用)生产者和消费者一起配合才能实现
性能要求高
(1)电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费
(阿里巴巴集团内部电商系统均使用局部顺序消息,既保证业务的顺序,同时又能保证业务的高性能)
顺序发布:对于指定的一个 Topic,客户端将按照一定的先后顺序发送消息
顺序消费:对于指定的一个 Topic,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。
注意:
顺序消息暂不支持广播模式(a机器快b机器慢不能保证哪个先到)
顺序消息不支持异步发送方式,否则将无法严格保证顺序(不能指定消息的顺序)
9、RocketMQ顺序消息
RocketMQ顺序消息的使用
- 生产端保证发送消息有序,且发送到同一个Topic的同个queue里面,RocketMQ的确是能保证FIFO的
例子:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,
根据MessageQueueSelector里面自定义策略,根据同个业务id放置到同个queue里面,
如订单号取模运算再放到selector中(arg参数),同一个模的值都会投递到同一条queue
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//如果是订单号是字符串,则进行hash,得到一个hash值
Long id = (Long) arg;
long index = id % mqs.size();//取模
return mqs.get((int)index);
}
- 消费端要在保证消费同个topic里的同个队列,不应该用MessageListenerConcurrently,(并发的消费)
应该使用MessageListenerOrderly,自带单线程消费消息,不能再Consumer端再使用多线程去消费,消费端分配到的queue数量是固定的,集群消会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。
官方例子 https://rocketmq.apache.org/docs/order-example/
public class OrderedProducer {
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
MQProducer producer = new DefaultMQProducer("example_group_name");
//Launch the instance.
producer.start();
String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
for (int i = 0; i < 100; i++) {
int orderId = i % 10;
//Create a message instance, specifying topic, tag and message body.
Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,
("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);
}
//server shutdown
producer.shutdown();
}
}
public class OrderedConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.registerMessageListener(new MessageListenerOrderly() {
AtomicLong consumeTimes = new AtomicLong(0);
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeOrderlyContext context) {
context.setAutoCommit(false);
System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
this.consumeTimes.incrementAndGet();
if ((this.consumeTimes.get() % 2) == 0) {
return ConsumeOrderlyStatus.SUCCESS;
} else if ((this.consumeTimes.get() % 3) == 0) {
return ConsumeOrderlyStatus.ROLLBACK;
} else if ((this.consumeTimes.get() % 4) == 0) {
return ConsumeOrderlyStatus.COMMIT;
} else if ((this.consumeTimes.get() % 5) == 0) {
context.setSuspendCurrentQueueTimeMillis(3000);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
10、RocketMQ顺序消息生产者投递
11、RocketMQ顺序消息消息者消费
RocketMQ顺序消息消费实战
MessageListenerConcurrently(多线程并发执行)
MessageListenerOrderly(单线程顺序执行)
现在是一个produce和一个consumer,如果有多个consumer怎么办??
consumer启动的时候,会到nameserver中拿取Topic,拿取对应的queue,一个Topic中有多个queue,Consumer会平均分配queue的数量
用IDE启动两个节点,
节点1 |节点2
Consumer会平均分配queue的数量(consumer数量<queue的数量)
并不是简单禁止并发处理,而是为每个Consumer Quene加个锁,消费每个消息前,需要获得这个消息所在的Queue的锁,这样同个时间,同个Queue的消息不被并发消费,但是不同Queue的消息可以并发处理(局部锁)
扩展思维:为什么高并发情况下ConcurrentHashMap比HashTable和HashMap更高效且线程安全?
提示:分段锁Segment
ConcurrentHashMap:分段锁
HashTable:整个map加锁