简介
主要概念
Producer:消息生产者,负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到Broker服务器。RocketMQ提供多种发送方式:同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认消息,单向发送不需要。
Consumer:负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息,并将其提供给应用程序。从用户应用的角度而言提供两种消费形式:拉取式消费、推动式消费
NameServer:名字服务器,充当路由消息的提供者。生产者或消费者能够通过名字服务器查询各主题相应的Broker IP列表,多个NameServer实例组成集群,但相互独立,没有信息交换
Brocker:消息中转角色,负责存储消息、转发消息。代理服务再RocketMQ这系统中负责就接受从生产者发送过来的消息并存储、同时为消费者的拉取请求做准备。代理服务器也存在存储消息相关的元数据,包括消费者组、消费进度偏移、主题和队列消息等
Message:消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中的每个消息拥有唯一的MessageID,且可以携带具有业务标识的key。系统提供了通过MessageID和key查询消息的功能
Topic:表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。
Tag:消息标志,用于同一主题下区分不同类型的消息。可以理解为Topic的二级分类,来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
单机下载安装
mkdir -p /app/rocketmq
# 进入rocketmq的官网获取到最新的mq的包,下载后传到服务器上 地址:https://rocketmq.apache.org/download/
unzip rocketmq-all-5.1.3-bin-release.zip
启动NameServer
修改启动脚本的jvm参数,默认的参数比较大,适用于生产环境
vim /app/rocketmq/rocketmq-all-5.1.3-bin-release/bin/runserver.sh
将
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
替换为
JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=16m -XX:MaxMetaspaceSize=40m"
启动nameServ脚本:
nohup sh /app/rocketmq/rocketmq-all-5.1.3-bin-release/bin/mqnamesrv &
查看日志:
tail -f ~/logs/rocketmqlogs/namesrv.log
启动Broker
这里启动单机版的Broker,先修改jvm参数
vim /app/rocketmq/rocketmq-all-5.1.3-bin-release/bin/runbroker.sh
将
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g"
替换为:
JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g"
修改Broker的配置文件:
vim /app/rocketmq/rocketmq-all-5.1.3-bin-release/conf/broker.conf
在文件的最后增加:
# brocker的注册地址
namesrvAddr = localhost:9876
# 这里有个坑,如果不指定地址的话,可能随机获取的地址访问不到,导致启动失败,这里明确的指定一下自己电脑的ip
brokerIP1=172.16.3.133
启动brocker的命令:
nohup sh /app/rocketmq/rocketmq-all-5.1.3-bin-release/bin/mqbroker -n localhost:9876 -c /app/rocketmq/rocketmq-all-5.1.3-bin-release/conf/broker.conf &
启动RocketMQ Dashborad
源码下载地址:https://github.com/apache/rocketmq-dashboard
下载之后本地编译:
mvn clean install -DskipTests
在linux中新建文件夹,并将本地生成的jar包和本地的application.yml放入文件夹中
mkdir -p /app/rocketmq/rocketmq-dashboard
[root@172 rocketmq-dashboard]# ls
application.yml /rocketmq-dashboard-1.0.1-SNAPSHOT.jar
使用如下指令启动jar:
nohup java -jar /app/rocketmq/rocketmq-dashboard/rocketmq-dashboard-1.0.1-SNAPSHOT.jar > output.log 2>&1 &
查看日志:
tail -f /app/rocketmq/rocketmq-dashboard/output.log
访问 http://127.0.,0.1:8080 即可访问rocketmq提供的界面
消息存储
CommitLog
真正储存消息的文件
消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;
ConsumeQueue
topic下每个queue对应消息的索引文件,相当于CommitLog的索引文件,索引是topic下的queue
消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;
IndexFile
CommitLog的索引文件,索引是key或者时间
IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:HOME/store/index/{fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故rocketmq的索引文件其底层实现为hash索引。
页缓存
页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。
内存映射
RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。
消息通信
processMessageReceived方法解析
- 根据协议码code获取对应的请求处理器,如果未获取到则使用缺省的请求处理器
- 判断请求处理器是否拒绝此请求
- 将请求数据封装为Runable对象提交到线程池
- 线程池执行处理逻辑
- 尝试执行RPC钩子before方法
- 使用在1中得到的请求处理器进行执行真正的请求处理方法(以Broker向NameServ注册自身信息为例,NameServ使用缺省的请求处理器进行执行)
- 请求命令处理完成后尝试执行RPC钩子after方法
- 判断此次请求是否是单向请求
- 如果不是单向请求则向对端返回响应结果
事务消息
half半消息:是指消息发送后,消息服务器将其标记为“半消息”状态,此时消息不回立即被消费,知道确认消息的状态后才会正式变为可消费的消息。主要的做法就是半消息有一个专门队列来储存消息
事务消息的状态
- COMMIT
- ROLLBACK
- UNKNOW
事务消息发送步骤如下:
-
生产者将事务消息发送给Broker
-
Broker接收到消息之后将对应消息包装为半消息(替换Topic和Queue)存储至CommitLog
-
Broker将消息持久化后,返回ack确认发送成功,此时消息暂不能投递,为半事务消息
-
生产者执行本地事务逻辑,TransactionListener#executeLocalTransaction
-
生产者将对应消息的事务处理结果EndTransactionRequestHeader发送给Broker,提交二次结果(commit或者rollback)
-
服务端收到确认结果后处理的逻辑如下:
- 二次确认结果为commit:服务端将半事务消息标记为可投递,存储到CommitLog中,并投递给消费者
- 二次确认结果为rollback:服务端将回滚事务,不会将半事务消息投递给消费者
在断网或者是声称这应用重启的特殊情况下,若服务器未收到发送这提交的二次确认结果,或者服务端收到的二次结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者发起消息回查。服务端仅仅会按照参数尝试指定次数,超过次数后事务会强制回滚,因此未解决回查时效性非常关键,需要按照业务的实际风险来设置。
事务消息回查步骤如下:
- 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果
- 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照上面的发送步骤4进行处理。
将数据放入半消息队列中的代码如下:
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
//储存原消息topic和queueId
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
String.valueOf(msgInner.getQueueId()));
msgInner.setSysFlag(
MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
//将消息放入半消息topic
msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
msgInner.setQueueId(0);
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
return msgInner;
}
broker返回成功之后,执行本地事务
switch (sendResult.getSendStatus()) {
case SEND_OK: {
try {
if (sendResult.getTransactionId() != null) {
msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
}
String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (null != transactionId && !"".equals(transactionId)) {
msg.setTransactionId(transactionId);
}
if (null != localTransactionExecuter) {
localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
log.debug("Used new transaction API");
//这里返回成功之后执行本地事务的方法
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
if (null == localTransactionState) {
localTransactionState = LocalTransactionState.UNKNOW;
}
if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
log.info("executeLocalTransactionBranch return {}", localTransactionState);
log.info(msg.toString());
}
} catch (Throwable e) {
log.info("executeLocalTransactionBranch exception", e);
log.info(msg.toString());
localException = e;
}
}
break;
执行本地事务后,根据返回的本地事务的状态,去尝试更新一次brocker中的消息状态
public void endTransaction(
final Message msg,
final SendResult sendResult,
final LocalTransactionState localTransactionState,
final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException {
final MessageId id;
if (sendResult.getOffsetMsgId() != null) {
id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId());
} else {
id = MessageDecoder.decodeMessageId(sendResult.getMsgId());
}
String transactionId = sendResult.getTransactionId();
final String destBrokerName = this.mQClientFactory.getBrokerNameFromMessageQueue(defaultMQProducer.queueWithNamespace(sendResult.getMessageQueue()));
final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(destBrokerName);
EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader();
requestHeader.setTransactionId(transactionId);
requestHeader.setCommitLogOffset(id.getOffset());
switch (localTransactionState) {
case COMMIT_MESSAGE:
//事务提交
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE);
break;
case ROLLBACK_MESSAGE:
//事务回滚
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE);
break;
case UNKNOW:
//事务未提交
requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE);
break;
default:
break;
}
doExecuteEndTransactionHook(msg, sendResult.getMsgId(), brokerAddr, localTransactionState, false);
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTranStateTableOffset(sendResult.getQueueOffset());
requestHeader.setMsgId(sendResult.getMsgId());
String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null;
//远程调用,去跟新消息的commitOrRollback字段
this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark,
this.defaultMQProducer.getSendMsgTimeout());
}
事务状态发送给brocker后,broker通过事务的状态进行处理数据
OperationResult result = new OperationResult();
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) {
//如果是已经提交的,直接commit消息
result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader);
if (result.getResponseCode() == ResponseCode.SUCCESS) {
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage());
msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback()));
msgInner.setQueueOffset(requestHeader.getTranStateTableOffset());
msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset());
msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp());
MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED);
//保存消息
RemotingCommand sendResult = sendFinalMessage(msgInner);
if (sendResult.getCode() == ResponseCode.SUCCESS) {
//删除半消息,将消息的标志置为 REMOVETAG(d)
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return sendResult;
}
return res;
}
} else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) {
//回滚消息
result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader);
if (result.getResponseCode() == ResponseCode.SUCCESS) {
RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader);
if (res.getCode() == ResponseCode.SUCCESS) {
//删除半消息,将消息的标志置为 REMOVETAG(d)
this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage());
}
return res;
}
}
response.setCode(result.getResponseCode());
response.setRemark(result.getResponseRemark());
return response;
延迟消息
延迟消息:延迟消息顾名思义不是用户能立即消费到的,而是等待一段特定的时间才能收到。
RocketMQ中的延迟级别:
级别: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
时长:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
启动broker的时候注册调度消息的方法,使用HookUtils#handleScheduleMessage
其中专门有针对延迟消息的处理,如果设置了延迟消息,将原topic和queueId保存下来后,将消息投递到topic为SCHEDULE_TOPIC_XXXX的系统topic中,queueId为0-17一共18个,分别针对1-18个延迟级别。
public static PutMessageResult handleScheduleMessage(BrokerController brokerController,
final MessageExtBrokerInner msg) {
final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
if (!isRolledTimerMessage(msg)) {
if (checkIfTimerMessage(msg)) {
if (!brokerController.getMessageStoreConfig().isTimerWheelEnable()) {
//wheel timer is not enabled, reject the message
return new PutMessageResult(PutMessageStatus.WHEEL_TIMER_NOT_ENABLE, null);
}
PutMessageResult tranformRes = transformTimerMessage(brokerController, msg);
if (null != tranformRes) {
return tranformRes;
}
}
}
// 针对延迟消息的处理 ******
// Delay Delivery
if (msg.getDelayTimeLevel() > 0) {
transformDelayLevelMessage(brokerController,msg);
}
}
return null;
}
/**
* 将原始队列存储之后,将 topic 改为 SCHEDULE_TOPIC_XXXX 中
* queueId为级别,默认 SCHEDULE_TOPIC_XXXX 中存储18个队列
*/
public static void transformDelayLevelMessage(BrokerController brokerController, MessageExtBrokerInner msg) {
if (msg.getDelayTimeLevel() > brokerController.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(brokerController.getScheduleMessageService().getMaxDelayLevel());
}
// Backup real topic, queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
msg.setTopic(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC);
msg.setQueueId(ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel()));
}
broker启动的时候会读取延迟队列的配置(默认18个延迟队列),然后开启18个线程进行遍历。
public void start() {
if (started.compareAndSet(false, true)) {
this.load();
this.deliverExecutorService = new ScheduledThreadPoolExecutor(this.maxDelayLevel, new ThreadFactoryImpl("ScheduleMessageTimerThread_"));
if (this.enableAsyncDeliver) {
this.handleExecutorService = new ScheduledThreadPoolExecutor(this.maxDelayLevel, new ThreadFactoryImpl("ScheduleMessageExecutorHandleThread_"));
}
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
Integer level = entry.getKey();
Long timeDelay = entry.getValue();
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}
if (timeDelay != null) {
if (this.enableAsyncDeliver) {
this.handleExecutorService.schedule(new HandlePutResultTask(level), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
}
//开始定时调度18个线程
this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
}
}
//每10s刷一次盘
this.deliverExecutorService.scheduleAtFixedRate(new Runnable() {
@Overr ide
public void run() {
try {
if (started.get()) {
ScheduleMessageService.this.persist();
}
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.brokerController.getMessageStore().getMessageStoreConfig().getFlushDelayOffsetInterval(), TimeUnit.MILLISECONDS);
}
}
每过100ms进行进行一次调度。因为延迟的时间都是相同的,所以没有排序的问题,每次都根据偏移量来判断就好。
当查询到消息到了推送的时间,mq会重新组装消息,去掉延迟的特性,组装成一个普通的消息立即投递。
public void executeOnTimeup() {
//找到对应的队列 18个
ConsumeQueueInterface cq =
ScheduleMessageService.this.brokerController.getMessageStore().getConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));
if (cq == null) {
this.scheduleNextTimerTask(this.offset, DELAY_FOR_A_WHILE);
return;
}
ReferredIterator<CqUnit> bufferCQ = cq.iterateFrom(this.offset);
if (bufferCQ == null) {
long resetOffset;
if ((resetOffset = cq.getMinOffsetInQueue()) > this.offset) {
log.error("schedule CQ offset invalid. offset={}, cqMinOffset={}, queueId={}",
this.offset, resetOffset, cq.getQueueId());
} else if ((resetOffset = cq.getMaxOffsetInQueue()) < this.offset) {
log.error("schedule CQ offset invalid. offset={}, cqMaxOffset={}, queueId={}",
this.offset, resetOffset, cq.getQueueId());
} else {
resetOffset = this.offset;
}
this.scheduleNextTimerTask(resetOffset, DELAY_FOR_A_WHILE);
return;
}
long nextOffset = this.offset;
try {
while (bufferCQ.hasNext() && isStarted()) {
CqUnit cqUnit = bufferCQ.next();
long offsetPy = cqUnit.getPos();
int sizePy = cqUnit.getSize();
long tagsCode = cqUnit.getTagsCode();
if (!cqUnit.isTagsCodeValid()) {
//can't find ext content.So re compute tags code.
log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
tagsCode, offsetPy, sizePy);
long msgStoreTime = ScheduleMessageService.this.brokerController.getMessageStore().getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
}
long now = System.currentTimeMillis();
//拿到延迟时间
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
long currOffset = cqUnit.getQueueOffset();
assert cqUnit.getBatchNum() == 1;
nextOffset = currOffset + cqUnit.getBatchNum();
long countdown = deliverTimestamp - now;
//时间还么到,小等100ms
if (countdown > 0) {
this.scheduleNextTimerTask(currOffset, DELAY_FOR_A_WHILE);
ScheduleMessageService.this.updateOffset(this.delayLevel, currOffset);
return;
}
//根据偏移量查询具体的消息
MessageExt msgExt = ScheduleMessageService.this.brokerController.getMessageStore().lookMessageByOffset(offsetPy, sizePy);
if (msgExt == null) {
continue;
}
//重新组装消息
MessageExtBrokerInner msgInner = ScheduleMessageService.this.messageTimeup(msgExt);
if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) {
log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}",
msgInner.getTopic(), msgInner);
continue;
}
boolean deliverSuc;
if (ScheduleMessageService.this.enableAsyncDeliver) {
deliverSuc = this.asyncDeliver(msgInner, msgExt.getMsgId(), currOffset, offsetPy, sizePy);
} else {
//默认走这个
deliverSuc = this.syncDeliver(msgInner, msgExt.getMsgId(), currOffset, offsetPy, sizePy);
}
if (!deliverSuc) {
this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
return;
}
}
} catch (Exception e) {
log.error("ScheduleMessageService, messageTimeup execute error, offset = {}", nextOffset, e);
} finally {
bufferCQ.release();
}
this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
}
详细的流程如下图:
死信消息
死信队列用于处理无法被正常消费的消息,当一条消息初次消费失败,消息队列会自动进行消息重试,达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立即将消息丢弃,而是将其发送到该消费者对应的特殊队列中。RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message)将存储死信消息的队列称为死信队列(Dead-Letter Queue)简称DLQ。如果消息延迟级别小于0,那么也会立即放入死信队列。
每个消费者组都设置一个Topic名称为%DLQ%+consumerGroup的死信队列,这里需要注意的是,和重试队列一样,这里的死信队列是针对消费者组,而不是针对每个Topic设置的。死信队列并不会一开始就创建,而是真正需求使用到的时候才会创建。
顺序消息
RocketMQ的顺序消息是通过消息队列来保证的,同一个消息队列中的消息是有序的,但是不同的队列之间是没有顺序保证的。当我们发送一条顺序消息时,RocketMQ会根据我们选择的算法来选择一个消息队列,然后将消息发送到这个队列中
当我们消费顺序消息时,RocketMQ会按照顺序从消息队列中读取消息。RocketMQ提供了两种方式来保证顺序消费:
- MessageListenerOrderly:一种高级的方式,可以确保一个线程只消费一个队列中的消息,并且消息按照发送的顺序被消费
- ConsumeOrderlyEnable:一种低级的方式,它只能确保一个进程消费一个队列中的消息,但是不能保证消息按照发送的顺序被消费
在RocketMQ中,有多种顺序算法可以选择。默认情况下,RocketMQ会使用轮询算法来选择消息队列。轮询算法会按照消息队列的顺序依次选择消息队列,并将消息发送到当前选中的队列中。如果当前选中的队列已经满了,RocketMQ会自动选择下一个队列。
除了轮询算法,RocketMQ还提供了一些其他的算法,比如哈希算法、随机算法、根据消息ID选择算法等。这些算法可以根据不同的需求选择,比如可以根据消息的业务属性来选择队列,以便确保消息被正确地处理。
顺序消费
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#consumeMessageDirectly
消息幂等
幂等:是指一个方法被多次重复执行的时候产生的影响和第一次执行的影响相同
消息重复的场景:
- 生产者发送消息时发生消息重复:当一条消息已经发送到broker里,并且已经持久化,此时出现断网或宕机,导致broker对生产者应答失败如果此时生产者意识到发送失败并尝试重新发送消息,消费者后续收到两条内容相同并且MessageID相同的消息
- 消费者消费消息时发生消息重复:消息已经投递到消费者并完成业务处理,此时出现断网或者宕机,导致broker没能收到返回的ack响应。此时broker认为消费者没有消费成功,为了能保证消息至少被消费一次,Brocker将在网络恢复后重新发送之前已经被处理过的消息。
- 负载均衡是发生消息重复:当Broker或者消费者重启、扩容时,都会触发新负载均衡,此时消费者去读取broker中的offset可能还没有及时更新,此时消费者可能会受到曾经被消费过的消息。
实现消费的幂等:由于做幂等操作不可避免要产生巨大的开销,RocketMQ 为了追求高性能,本身没有提供消费幂等的特性,它要求我们在业务上进行去重,也就是说自己在消费消息时要做到幂等性。RocketMQ 虽然不能严格保证不重复,但是正常情况下很少会出现重复发送、消费重复情况,只有网络异常,Consumer 启停等异常情况下会出现消息重复。 所以消费者在接收到消息以后,有必要根据业务上的唯一 Key 对消息做幂等处理的必要性。
通用解决方案:使用数据库+Redis实现消息消费幂等
- 消费者消费时,拿到唯一的业务标识key,通过key去Redis中查询是否存在对应的记录,如果存在就说明有重复
- 根据key去数据库查询,如果数据库中存在,就说明本次存在重复
- 将key作为唯一键插入数据库,并设置唯一索引
RocketMQ集群
多Master模式
一个集群无Slave,全是Master,例如2个Master或者3个Master,这种模式的优缺点如下:
-
优点:配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高;
-
缺点:单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
### 在A机器上启动第一个master,例如NameServer的IP为:192.168.1.1
nohup sh bin/mqbroker -n 192.168.1.1:9876 -c ./conf/2m-noslave/broker-a.properties &
#### 在B机器上启动第二个master,例如NameServer的IP为:192.168.1.1
nohup sh bin/mqbroker -n 192.168.1.1:9876 -c ./conf/2m-noslave/broker-b.properties &
多Master多Slave模式-异步复制
每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级),这种模式的优缺点如下:
- 优点:即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样;
- 缺点:Master宕机,磁盘损坏情况下会丢失少量消息。
### 在机器A,启动第一个Master,例如NameServer的IP为:192.168.1.1
$ nohup sh mqbroker -n 192.168.1.1:9876 -c ./conf/2m-2s-async/broker-a.properties &
### 在机器B,启动第二个Master,例如NameServer的IP为:192.168.1.1
$ nohup sh mqbroker -n 192.168.1.1:9876 -c ./conf/2m-2s-async/broker-b.properties &
### 在机器C,启动第一个Slave,例如NameServer的IP为:192.168.1.1
$ nohup sh mqbroker -n 192.168.1.1:9876 -c ./conf/2m-2s-async/broker-a-s.properties &
### 在机器D,启动第二个Slave,例如NameServer的IP为:192.168.1.1
$ nohup sh mqbroker -n 192.168.1.1:9876 -c ./conf/2m-2s-async/broker-b-s.properties &
多Master多Slave模式-同步双写
每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功,这种模式的优缺点如下:
- 优点:数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;
- 缺点:性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。