上一篇我们分析介绍了RocketMq利用MappedFile对一个文件的操作,这里我们继续分析RocketMq如何管理文件集。
MappedFileQueue就是RocketMq封装的对有序文件队列的管理,其主要用于CommitLog,ConsumeQueue,IndexFile集合的操作管理。
MappedFileQueue相关属性
public class MappedFileQueue {
// 批量删除文件最大数量
private static final int DELETE_FILES_BATCH_MAX = 10;
// 存儲路徑
private final String storePath;
// 目录下每个文件的大小 (commitLog:1G,consumeQueue:30w*20bytes, indexFile:400M 约2000w数据)
protected final int mappedFileSize;
// 目录下的mappedFile对象集合
protected final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<MappedFile>();
// 创建mappedFile的服务,内部有自己的线程 咱们通过向它提交request 内部线程处理完后会返回结果,就是mappedFile对象
private final AllocateMappedFileService allocateMappedFileService;
// 当前目录下,刷盘位点(currentMappedFile.name + wrotePosition)
protected long flushedWhere = 0;
// 当前目录下的
private long committedWhere = 0;
// 最后一条消息的存储时间
private volatile long storeTimestamp = 0;
public MappedFileQueue(final String storePath, int mappedFileSize,
AllocateMappedFileService allocateMappedFileService) {
this.storePath = storePath;
this.mappedFileSize = mappedFileSize;
this.allocateMappedFileService = allocateMappedFileService;
}
}
storePath定义了管理文件集路径
mappedFileSize定义了单个文件的大小
mappedFiles管理关联着文件夹下所有的文件
其中定义了多个比较重要的方法
MappedFileQueue# load()加载方法
MappedFileQueue# flush(int)} 刷盘
MappedFileQueue# getLastMappedFile() 当前正在写入的mappedFile,不存在走创建逻辑
MappedFileQueue# findMappedFileByOffset(long, boolean) 根据offset查找目标mappedFile对象
MappedFileQueue# deleteExpiredFileByTime(long, int, long, boolean) 根据offset查找目标mappedFile对象 commitLog删除逻辑
加载
遍历文件夹,将所有文件的链接封装为MappedFile对象放入mappedFilesList对象中。
public boolean load() {
File dir = new File(this.storePath);
File[] ls = dir.listFiles();
if (ls != null) {
// 遍历
return doLoad(Arrays.asList(ls));
}
return true;
}
public boolean doLoad(List<File> files) {
// ascending order
files.sort(Comparator.comparing(File::getName));
// 迭代
for (File file : files) {
if (file.length() != this.mappedFileSize) {
log.warn(file + "\t" + file.length() + " length not matched message store config value, please check it manually");
return false;
}
try {
// 建立mappedFile对象
MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
// 添加到管理集合
this.mappedFiles.add(mappedFile);
log.info("load " + file.getPath() + " OK");
} catch (IOException e) {
log.error("load file " + file + " error", e);
return false;
}
}
return true;
}
获取末尾的文件
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
// 文件起始偏移量
long createOffset = -1;
// 先尝试获取
MappedFile mappedFileLast = getLastMappedFile();
if (mappedFileLast == null) {
// 没有获取到
createOffset = startOffset - (startOffset % this.mappedFileSize);
}
// 存在 但是满了
if (mappedFileLast != null && mappedFileLast.isFull()) {
createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
}
// 创建新文件
// 1.list为空
// 2.最近的文件写满了
if (createOffset != -1 && needCreate) {
return tryCreateMappedFile(createOffset);
}
return mappedFileLast;
}
文件不存在就创建tryCreateMappedFile
protected MappedFile tryCreateMappedFile(long createOffset) {
// 文件名
String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
//
String nextNextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset
+ this.mappedFileSize);
// do create
return doCreateMappedFile(nextFilePath, nextNextFilePath);
}
protected MappedFile doCreateMappedFile(String nextFilePath, String nextNextFilePath) {
MappedFile mappedFile = null;
if (this.allocateMappedFileService != null) {
// 用allocateMappedFileService创建
// 当mappedfileSize>1G 创建会预热,在空页上写零值,物理页就会先创建出来
mappedFile = this.allocateMappedFileService
.putRequestAndReturnMappedFile(nextFilePath,nextNextFilePath, this.mappedFileSize);
} else {
try {
// 直接new
mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
} catch (IOException e) {
log.error("create mappedFile exception", e);
}
}
if (mappedFile != null) {
//
if (this.mappedFiles.isEmpty()) {
// 目录下的首文件
mappedFile.setFirstCreateInQueue(true);
}
// 添加到目录list
this.mappedFiles.add(mappedFile);
}
return mappedFile;
}
获取文件操作,隐含着创建文件的逻辑。
刷盘操作
/**
*
* @param flushLeastPages 0:表示强制刷新 >0:表示脏页达到flushLeastPages才刷脏
* @return true:实际没刷数据,false:实际刷了数据
*/
public boolean flush(final int flushLeastPages) {
boolean result = true;
//
MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0);
if (mappedFile != null) {
// 找到了 刷盘
long tmpTimeStamp = mappedFile.getStoreTimestamp();
// 刷盘
int offset = mappedFile.flush(flushLeastPages);
// 更新刷盘位点
long where = mappedFile.getFileFromOffset() + offset;
// true:实际没刷数据,false:实际刷了数据
result = where == this.flushedWhere;
this.flushedWhere = where;
if (0 == flushLeastPages) {
this.storeTimestamp = tmpTimeStamp;
}
}
return result;
}
提交
public boolean commit(final int commitLeastPages) {
boolean result = true;
// 找到上次提交的文件
MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0);
if (mappedFile != null) {
int offset = mappedFile.commit(commitLeastPages);
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.committedWhere;
this.committedWhere = where;
}
return result;
}
这里看到有提交消息就返回false
清理文件
/**
* consumeQueue删除逻辑
* @param offset offset
* @param unitSize
* @return
*/
public int deleteExpiredFileByOffset(long offset, int unitSize) {
// 拷贝
Object[] mfs = this.copyMappedFiles(0);
//
List<MappedFile> files = new ArrayList<MappedFile>();
int deleteCount = 0;
if (null != mfs) {
// 保证正在写的数据不被删除
int mfsLength = mfs.length - 1;
//
for (int i = 0; i < mfsLength; i++) {
boolean destroy;
MappedFile mappedFile = (MappedFile) mfs[i];
// 当前文件的最后一个数据单元
SelectMappedBufferResult result = mappedFile.selectMappedBuffer(this.mappedFileSize - unitSize);
if (result != null) {
// 最后一条数据的commitLog物理偏移量
long maxOffsetInLogicQueue = result.getByteBuffer().getLong();
result.release();
// true:当前queue都是过期数据
destroy = maxOffsetInLogicQueue < offset;
if (destroy) {
log.info("physic min offset " + offset + ", logics in current mappedFile max offset "
+ maxOffsetInLogicQueue + ", delete it");
}
} else if (!mappedFile.isAvailable()) { // Handle hanged file.
log.warn("Found a hanged consume queue file, attempting to delete it.");
destroy = true;
} else {
log.warn("this being not executed forever.");
break;
}
if (destroy && mappedFile.destroy(1000 * 60)) {
// 删除记录
files.add(mappedFile);
// 删除量
deleteCount++;
} else {
break;
}
}
}
deleteExpiredFile(files);
return deleteCount;
}
/**
* commitLog删除逻辑
* @param expiredTime 过期时间
* @param deleteFilesInterval 删除两个文件直接的时间间隔
* @param intervalForcibly mf.destory传递的参数
* @param cleanImmediately 强制删除
* @return
*/
public int deleteExpiredFileByTime(final long expiredTime,
final int deleteFilesInterval,
final long intervalForcibly,
final boolean cleanImmediately) {
// 拷贝
Object[] mfs = this.copyMappedFiles(0);
if (null == mfs)
return 0;
int mfsLength = mfs.length - 1;
int deleteCount = 0;
//
List<MappedFile> files = new ArrayList<MappedFile>();
if (null != mfs) {
// 迭代遍历
for (int i = 0; i < mfsLength; i++) {
//
MappedFile mappedFile = (MappedFile) mfs[i];
// 文件最大存活截止时间点
long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
// cleanImmediately 上层会判断磁盘占用率达到95%,会设置为true
//
if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
// 删除
if (mappedFile.destroy(intervalForcibly)) {
// 删除的文件
files.add(mappedFile);
// 删除数量
deleteCount++;
// 达到最大删除数 10
if (files.size() >= DELETE_FILES_BATCH_MAX) {
break;
}
if (deleteFilesInterval > 0 && (i + 1) < mfsLength) {
try {
// 删除需要点时间
Thread.sleep(deleteFilesInterval);
} catch (InterruptedException e) {
}
}
} else {
break;
}
} else {
//avoid deleting files in the middle
break;
}
}
}
deleteExpiredFile(files);
return deleteCount;
}
删除有两种方式
根据偏移量删除,例如ConsumeQueue的删除策略,ConsumeQueue中每条消息的长度固定是20字节,可以根据逻辑上是第几条消息进行删除操作。
根据过期时间清除,如RocketMq中存存消息的日志文件CommitLog文件,其根据文件的最后修改时间来确定是否删除文件。