之前在学习rocketmq源码的时候,阅读了丁老师的《rocketmq技术内幕》
在写到broker异常关闭再恢复的情况下会造成大量的消息重复消费问题。我认为这里是有误的。
首先我们知道无论broker是否正常异常恢复,都是先恢复逻辑队列CousumeQueue的。然后再根据本地store目录下是否有abort文件来判断关闭的时候是正常还是异常。
abort:当我们正常启动一个broker时(包括恢复和创建),如果是恢复的情况下会在恢复完成后在store目录下创建一个abort文件。如果不是恢复,则会跳过恢复的过程然后在store文件下创建一个abort文件。这个abort文件的作用是当broker正常关闭的情况下,代码中事先注册了一个jvm的钩子函数,会调用销毁这个abort文件的方法。如果你在broker没启动的情况下,去broker服务器上去检查本地文件,在store目录下是否有abort文件。如果有则证明是异常关闭,反之是正常关闭。
当恢复完CousmerQueue逻辑队列后,开始恢复broker。broker恢复的逻辑是根据是否有abort文件的存在与否来进行判断走异常恢复的流程还是走正常恢复的流程。这里就不BB正常恢复的流程了。
异常恢复流程:
读取commitlog文件,从倒数第一个开始从后向前遍历,判断commitlog队列中第一条消息的magic_code是否符合条件,落盘时间是否大于0,落盘时间是否小于checkpoint文件中的最新落盘时间。这个checkpoint最新落盘时间一般都是在reputMessage的时候触发更新的,即往Consumequeue逻辑消息队列中写数据的时候。还有就是正常关闭的时候 会在最后更新一下这个时间。
当三个条件都满足的情况下将这个commitlog取出。(这个commitlog如果这三个条件满足说明它前面的commitlog肯定是正确无误的,因为commitlog是顺序写的,一个写完接下去写下一个。)
之后从这个commitlog文件的头部开始读取消息,一条接着一条读, 每条读取之后先做验证,通过判断消息的mgic_code,消息长度是否符合,符合条件的话将其包装成dispatchRequest 转发给对应的逻辑队列CousumeQueue进行持久化。
书中认为当dispatchRequest转发持久化后会发生大量重复的逻辑消息产生,这应该是错误的。下面是转发之后dispathRequest在Cousumequeue中的处理流程源码解析。
private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
final long cqOffset) {
/*这个offset是dispatchRequest中消息的物理偏移量,maxPhysicOffset是此Cousmequeue中存储的逻辑信息中筛选出来的最大物理偏移量。
如果说转发过来的dispathRequest的offset小于maxPhysicOffset 那么必然这个逻辑消息已经持久化成功并且这边直接返回了而不走下面
的代码去将这条dispatchRequest转化成逻辑消息添加到Cousmequeue当中,从而造成Cousmequeue中会有两个元数据包含了相同的物理偏移量。
那如果说这个offset物理偏移量大于这个会怎么样呢继续看下去。
* */
if (offset <= this.maxPhysicOffset) {
return true;
}
//这个byteBufferIndex是一个ByteBuffer,用来临时装载将要添加持久化的逻辑消息。一个逻辑消息的组成(8字节物理偏移量,4字节物理消息长度,8字节
// 消息tag的hashcode)
this.byteBufferIndex.flip();
this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
this.byteBufferIndex.putLong(offset);
this.byteBufferIndex.putInt(size);
this.byteBufferIndex.putLong(tagsCode);
//cqOffset即存在commitlog队列中的一条物理消息在Cousmequeue中的逻辑偏移量,乘以20就得到逻辑队列中实际的偏移量。
final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE;
//mappedFileQueue在这里代表一个ConsumeQueue(比如topic为topicA,quueueId为1的Consuemequeue),一个mappedFile为
//此CousumeQueue最新的一个文件操作对象,这里可能说的有点奇怪,可能这里会有疑惑为什么一个CosumerQueue对应的是一个
//mappedFileQueue,我们这里要明白一个事就是一个ConsumeQueue再创建时会先分配一个30W*20bit的空间,提前分配好能够利用
//内存映射技术快速存取,那么如果你针对这个逻辑队列ConsumeQueue的消息量超过了这个空间就必须再继续创建30W*20bit的空间,
//一个mappedFile对应一个30W*20bit的空间。取出最新也就是最后的那个mappedFile即是最新的那个可以写的mappedFile。我们都是通过该
//操作mappedFile去完成逻辑消息的持久化或者读取,包括物理Commitlog等只要涉及和磁盘交互的都是由MappedFile对象去完成。
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
if (mappedFile != null) {
//此逻辑是对于消费者发送消息后需要关注的,对于恢复而言暂且不看。
if (mappedFile.isFirstCreateInQueue() && cqOffset != 0 && mappedFile.getWrotePosition() == 0) {
this.minLogicOffset = expectLogicOffset;
this.mappedFileQueue.setFlushedWhere(expectLogicOffset);
this.mappedFileQueue.setCommittedWhere(expectLogicOffset);
this.fillPreBlank(mappedFile, expectLogicOffset);
log.info("fill pre blank space " + mappedFile.getFileName() + " " + expectLogicOffset + " "
+ mappedFile.getWrotePosition());
}
//dispatchRequest中逻辑偏移量若大于0,走此逻辑
if (cqOffset != 0) {
//得到此CosumeQueue恢复后已写最大逻辑偏移量
long currentLogicOffset = mappedFile.getWrotePosition() + mappedFile.getFileFromOffset();
//如果说此条消息的逻辑偏移量小于最大已写逻辑偏移量,那肯定可以保证此逻辑消息已经写入了此队列当中,虽然会报日志错误,
//但最终直接返回true而不去执行后面的append操作。实际上我认为 上面的if (offset <= this.maxPhysicOffset)已经帮我们
//判断好了 这一个判断条件理论上是走不进来的。因为如果expectLogicOffset < currentLogicOffset 那么必然offset <= this.maxPhysicOffset
if (expectLogicOffset < currentLogicOffset) {
log.warn("Build consume queue repeatedly, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
expectLogicOffset, currentLogicOffset, this.topic, this.queueId, expectLogicOffset - currentLogicOffset);
return true;
}
//如果expectLogicOffset >= currentLogicOffset则 此条准备写入的逻辑消息对应的物理消息偏移量肯定不存在于此Cosumequeue当中的。
//所以会写入,且不会有重复发生。
if (expectLogicOffset != currentLogicOffset) {
LOG_ERROR.warn(
"[BUG]logic queue order maybe wrong, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
expectLogicOffset,
currentLogicOffset,
this.topic,
this.queueId,
expectLogicOffset - currentLogicOffset
);
}
}
this.maxPhysicOffset = offset;
return mappedFile.appendMessage(this.byteBufferIndex.array());
}
return false;
}