前言:本文为原创 若有错误欢迎评论!
- 安装参考我的博客:https://blog.csdn.net/weixin_43934607/article/details/100538881
- 整合SpringBoot参考我的博客:https://blog.csdn.net/weixin_43934607/article/details/100115270
一.主从模式搭
- 准备两台虚拟机
1.主从模式搭建
-
先进入bin 启动nameserver:
nohup sh mqnamesrv &
-
进入每个机子的:/conf/2m-2s-async/
cd /conf/2m-2s-async/
-
主节点编辑broker-a.properties 或 broker-b.properties
vi broker-a.properties
增加:namesrvAddr=192.168.159.129:9876;192.168.159.130:9876 修改:brokerClusterName=myCluster
-
从节点编辑broker-a-s.properties 或 broker-b-s.properties
vi broker-a-s.properties
增加:namesrvAddr=192.168.159.129:9876;192.168.159.130:9876 修改:brokerClusterName=myCluster
注意:一个机子只用给集群配置
- namesrvAddr:相当于注册中心 集群中不用所有节点都启动nameserver 格式为:ip1:端口;ip2:端口
- brokerClusterName:通过nameservere找到连接的节点 然后通过这个名字用来区分是否是一个集群
-
进入bin目录
- 主节点启动不带’-s‘的配置文件
nohup sh mqbroker -c …/conf/2m-2s-async/broker-a.properties &
- 从节点启动带’-s‘的配置文件
nohup sh mqbroker -c …/conf/2m-2s-async/broker-a-s.properties &
- 主节点启动不带’-s‘的配置文件
-
修改可视化管控台配置和使用时的改变
-
进入rockemq-console-ng.jar的/BOOT-INF/classes 修改application.properties的
namesrvAddr=192.168.159.129:9876;192.168.159.130:9876
-
在应用里给DefaultMQProducer和DefaultMQConsumer设置nameserver的时候 也是多个ip地址的形式:
(192.168.159.129:9876;192.168.159.130:9876)
-
2.消费问题
- 主节点宕机之后 可以从从节点消费 主节点再次上线之后会读取从节点的偏移量 保证不会重复消费
- 注意:slave有一定滞后性 在主节点宕机后可能有少量消息丢失 主节点重新上线后未同步的消息可以恢复消费
3.broker如何配置主从
-
一个master可以对应多个slave
- 通过brokerName(不是brokerClusterName)来匹配
- 通过不同brokerId(主:0,从:1、2、3递增)来对应主从
- 因为/conf/2m-2s-async/ 的brokerName都设置好了
- broker-a.properties和broker-a-s.properties的brokerName都是broker-a
- broker-b.properties和broker-b-s.properties的brokerName都是broker-b
-
注意:
- 集群的节点数 必须小于等于每个topic的队列数(因为负载均衡策略用不了 是每次消息来的时候进行分配 大于队列总数永远无法满足所有节点同时运作)
二.生产者核心知识
1.消息发送状态:
- FLUSH_DISK_TIMEOUT:只有同步刷盘超时才会报
- FLUSH_SLAVE_TIMEOUT:同步复制超时
- SLAVE_NOT_AVAILABLE:同步复制 子节点宕机
- SEND_OK:发送成功
2.消息重试
-
生产者:
- 本身支持重试机制 默认三次如果网络差可以多设置两次
- 只有同步发送才会重试,异步发送、单向发送都不会重试 只发送一次
- 设置重试次数:defultMQProducer.setRetryTimesWhenSendFailed()
- 超过重试次数 抛出异常 扔进死信队列
- 也可以设置消息的超时时间 但是一旦超时 就不会重试 会抛出异常 扔进死信队列
- 设置超时时间:defultMQProducer.send(msg,超时时间(单位毫秒))
- 消息发送失败的概率很低 所以一般不用特别关注 监听死信队列 定期补偿就可以
- 本身支持重试机制 默认三次如果网络差可以多设置两次
-
消费者:
-
excption机制
-
集群消费(默认):
-
如果返回的是"RECONSUME_LATER"或者有异常抛出 消息会重回broker进入重试队列再发送 下次接收到该消息的msgId也会改变
-
用MessageExt.getReconsuTimes()获得重试的次数
默认是最多16次(messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h) -
但一般判断次数大于3次就 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS 然后记录日志然后人工补偿 如果大于16次回进入死信队列
-
-
广播消费:
- consumer.setMessageMode(MessageMode.CLUSTERING)
- 异常或者RECONSUME_LATER之后不会重回broker重试
-
-
timeout机制
- 设置消费超时时间(分钟)
- consumer.setConsumeTimeout(RocketMQConstant.CONSUMER_TIMEOUT_MINUTES);
- 如果Send_OK到了消费者 ,但在消费者的消费的超时时间内没有返回状态,这个时候会被认作timeout,rocketmq会进进入重试队列 重新发送该消息 大于16次会进入死信队列
- 设置消费超时时间(分钟)
-
3.异步发送消息
defaultMQProdvider.send(Message,new SendCallback(){
onSuccess(SendResult){ }、
onException(){
//根据业务判断是否继续重试(异步不重试 只发一次消息)
}
))
4.发送方式oneway
- defaultMQProducter.send():都会有服务器相应发送状态和回调函数 保证消息不会丢失
- defaultMQProducter.oneway():发送没有服务器返回状态 但是最快不阻塞 用作logserver
5.延迟消息
- message.setDelayTimeLevel(level)
- 级别从1开始对应后面的时间:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
- 使用场景:定时提醒、定时任务
6.MessageQueneSelector
- 指定一个topic下的指定队列 不指定是随机分配
defaulrMQProducter.send(Message,new MessageQueneSelector(){
select(List<MessageQueue> mqs, Message msg, Object arg){
int queueNum = Integer.valueOf(String.valueOf(arg)) % 4;
System.out.println("队列id:"+queueNum+" 消息:"+new String(msg.getBody()));
return mqs.get(queueNum);
}
},args,new SendCallback(){ } )
- 其中的args是自己传入的参数 指定的队列的下标(从0开始 不能越界 如果是用程序自动创建的topic默认有4个)
- 可以再加一个SendCallback()参数来异步发送(该参数非必须)
三.消费者核心知识
1.顺序消息
- RocketMQ是局部消息满足顺序消息 即指定的topic的每一个消息队列在只能由一个线程处理 不会把一个队列里的消息给多个线程 保证一个线程处理的时候消息的完整与顺序
defaultMQConsumer.registerMessageListner(new MessageListnerOrderly(){ } )
-
默认的消费方式是并发消费:new MessageListenerConcurrently
即:为了保证顺序 把所有有顺序的消息放在一个队列中 然后按顺序接收 但是不可以异步发送消息 必须同步 -
注意:
不是禁止并发消费 是给每个单独的队列加个锁 把每个队列轮询分配给不同节点处理(分段锁的思想)
2.消息过滤
- tag标签
defaultMQConsumer.subscribe(topic,“tag1 || tag2 || tag3”)
- sql92
- 在broker开启
主节点编辑 /conf/2m-2s-async/broker-a.properties
从节点编辑 /conf/2m-2s-async/broker-a-s.properties
在配置最后加一行:enablePropertyFilter=true
-
重新带配置文件启动
-
使用:
给要发送的消息:message.putUserProperty(name,amount)
在消费者订阅的时候:
defaultMQConsumer.subscribee(topic,MessageSelector.bySql(“amount > 5”)
-
注意:name对应的amount是String类型 然后订阅的时候不一定是比大小 就和普通sql语句一样 (">、<、 =、IS NULL、AND、OR、NOT等都支持)
-
建议:一个topic单一职责 不用tag和sql92去过滤 直接订阅tag为"*"即可
3.DefaultMQPushConsumer和DefaultMQPullConsumer
- pushConsumer:
-
本质是长轮询(即consumer主动跟broker建立一个链接拉取数据 这个链接在15s内如果有消息就会自动推送到consumer)折中了push的一有消息就推送、pull的每次都要拉取
-
收到消息自动处理并改变offset 如果有新的consumer加入会自动负载均衡
-
支持优雅的关闭 consumer.shutdown() 会自己保存消息的偏移量offset
(用SpringBoot的上下文监听器,或者@PostConstruct @PreDestory 来开启和关闭consumer)
- pullConsumer:
-
更加灵活可控 每次自己拉取 编码复杂度高
-
需要每次把offset保存在内存或者磁盘 下次再比对
四.Offset和CommitLog
1.offset
- 当消息的接受为广播模式把消息存储模式设置为本地存储(因为每个节点都要消费topic下所有消息 所以按照各个节点自己的进度消费)
consumer.setMessageMode(MessageMode.BROADCASTING)
consumer.setOffsetStore(OffsetStore.LocalFileOffsetStore)
- 即:每次每个节点读取消息的时候从本地读取并增加自己的offset 不从broker读取消息的偏移 (同样pushConsumer也会维护本地存储的offset)
2.commitLog
-
是消息真正存储的物理文件
每个消息队列文件的最终持久化地址:/root/store/CommitLog/
-
每个消息队列存储消息的临时地址:
/root/store/consumequeue/[topicName]/[queneId]/ 存储的临时的文件也会被持久化到commitLog
每个文件的名字是偏移量起始地址 最大1G=102410241024 每存满1G 就会先建立一个文件 文件名是102410241024的值
- 从consumequeue读取索引 然后通过索引到commitLog取数据
- 集群配置文件的fileReserveTime设置的是消息保存的时间 删了之后offset不会减少
五.分布式消息事务
- 发送消息到broker之后有一回调 发送发二次确认之后决定Commit还是Rollback
1.发送二次确认的三种状态:
- LocalTransactionState.COMMIT_MESSAGE:提交消息
- LocalTransactionState.ROLLBACK_MESSAGE:回滚消息
- LocalTransactionState.UNKNOW:过一段时间执行回查方法
注意:
- 必须同步发送消息
- 事务性消息必须单独有一个groupName 不能和其他共享一个groupName
2.使用(只是针对producer):
- 定义一个内部类:
class TransactionListenerImpl implments TransactionListener{
@Override
public LocalTransactionState executeLocalTransaction(Message message,Object arg){
//message就是那个半发送的消息 arg是在transcationProducter.send(Message,Object)时的另外一个携带参数)
//执行本地事务或调用其他为服务
if(...) return LocalTransactionState.COMMIT_MESSAGE
if(...) return LocalTransactionState.ROLLBACK_MESSAGE
//如果在检查事务时数据库出现宕机可以让broker过一段时间回查 和return null 效果相同
if(...) return LocalTransactionState.UNKNOW
}
@Override
public LocalTransactioonState checkLocalTransaction(MessageExt msg){
//只去返回commit或者rollback
}
}
- 定义一个线程池 让broker用来执行回调和回查
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000),
new ThreadFactory() { //给每个线程设置一个名字方便找bug
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
- 新建事务性producer:
new TransactionMQProducer(groupName);
- 设置groupName、namesrvAdd
- 在多设置executorService、transacionListener
- 调用.start()启动(这个init方法应该用@PostStruct注解)
producer.setNamesrvAddr(JmsConfig.NAME_SERVER);
producer.setExecutorService(executorService);
producer.setTransactionListener(new transactionListenerImpl());
producer.start();
- 在Producer的类中定义一个getProducer方法 在controller中注入该类之后拿到
transactionMQProducer:
transactionMQProducer.sendMessageTransaction(Message,Object)
六.双主双从配置
- 推荐同步复制 异步刷盘 同步投递消息
- 刷盘:每个messageQuene把消息持久化到commitLog
- 复制:持久到conmmitLog后是否立即同步到从节点
- producer投递方式:producer的send方法是否异步使用)
- 配置讲解
# 注册中心nameserver地址 namesrvAddr=192.168.56.129:9876;192.168.56.130:9876 # 集群名称(很重要 一个集群的brokerClusterName必须相同) brokerClusterName=ItcastCluster # broker名称(相同brokerName是主从) brokerName=broker01 # broker的角色(相同brokerName下 0是主 其余1、2、3...是从) brokerId=0 # 什么时候删除过期文件(04指凌晨四点) deleteWhen=04 # 文件保存多长时间(小时) fileReservedTime=48 # broker角色 brokerRole=SYNC_MASTER # 刷盘方式 flushDiskType=ASYNC_FLUSH # 本虚拟机ip 用于远程访问broker brokerIP1=192.168.56.129 # 本虚拟机ip 用于主从之间访问同步 brokerIp2=192.168.56.129 # 监听的端口(会占用三个端口 listenPort-2、listenPort、listenPort+1) # 如果是用docker部署在一台机子上 那么集群每个节点监听的端口要不同 # listenPort=10911
1.准备四台虚拟机,先按照一配置好一对主从(这对主从的nameserver服务都启动)
brokerRole=SYNC_MASTER(只针对主节点 改为同步复制)
2.在开两个节点(不开启nameserver)
3.分别只配置-b.properties和-b-s.properties
增加:
namesrvAddr=192.168.159.129:9876;192.168.56.130:9876 (相当于注册中心 只要有开启的能够去注册就可以 不用全部开启)
修改:
brokerClusterName=集群名
brokerRole=SYNC_MASTER (只针对主节点 改为同步复制)
4.带配置启动
5.所有的broker的设置都是通过各自集群的文件来设置的
-
推荐配置:
defaultTopicQueueNums=4
#线上应该禁止自动创建topic
autoCreateTopicEnable=false#线上应该禁止自动创建订阅组
autoCreateSubscriptionGroup=false#复制方式改为同步
brokerRole=SYNC_MASTER#刷盘方式改为异步
flushDiskType=ASYNC_FLUSH#每个节点都有从节点
6.过程:
- 各个节点在nameserver注册 然后就可以相互访问
- 通过brokerClusterName在nameserver的ip中找到一个相同的集群
- 找到集群后通过brokerName找到对应的主从节点 然后通过brokerId判断是主还是从
七.常见问题
1.消息重复消费
-
Redis:
- 每次发送的message都设置唯一的key(message.setKey( ))来标识 在接收到Message后把这个唯一的key放到redis中 每次通过setnx判断是否有接收过此消息
- 注意:这个消息去重不用考虑原子性问题 但Redis作分布式锁要考虑原子性问题 就要所有在加锁、解锁时 让拿数据和操作数据两个命令一起执行 所以必须使用lua脚本)
-
Mysql:
新建一个去重表 以Message的key作为表的唯一索引 然后给这张表每次添加添加key 并且捕获异常 如果抛出异常就证明已经消费过了
2.可靠性保证
-
producer:不使用oneway方式发送消息 并且给Message设置唯一的key(为了重试的key唯一)
-
broker:双主双从、并且启动多个nameserver、同步双写 异步刷盘
-
consumer:消息务必保留日志(即消息体和元数据)、做好幂等性(即发送多次相同消息 都只处理一次)
3.消息堆积
- 新建一个临时topic 把quene的数目增大 把consumer也增加多 然后把堆积的消息都用这个topic消费
4.推荐微服务技术选型:
-
框架:SpringBoot+SpringCloud/Dubbo+RocketMQ+Redis
-
部署:nginx+Docker+阿里云
-
数据库:mysql主从
-
性能测试:Jmeter