参考资料
- 官网文档 github.com/apache/rock…
- 书籍 《RocketMQ分布式消息中间件:核心原理与最佳实践 》
- 源码
并非直接就读的源码,之前已经看过文档、资料,就是书上和官网的东西。
同时对broker和namesrv下的源码简单浏览过,本身上层应用的逻辑还是比较简单的。
MappedFile里都是些啥
简单说MappedFile提供了读写文件操作,读写一个MappedFile对象就是在读写一个磁盘上的文件。
都定义了哪些属性啊?
// 页的大小 4k 还是 4b?
public static final int OS_PAGE_SIZE = 1024 * 4;
protected static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
// 总计虚拟内存大小
private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
// 文件数量
private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
// 三个 positation,还不晓得怎么用,再看看
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
protected final AtomicInteger committedPosition = new AtomicInteger(0);
private final AtomicInteger flushedPosition = new AtomicInteger(0);
// 文件大小
protected int fileSize;
// 通道
protected FileChannel fileChannel;
/**
* Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
* 消息都会放在这里,然后再写入channel
*/
protected ByteBuffer writeBuffer = null;
protected TransientStorePool transientStorePool = null;
private String fileName;
private long fileFromOffset;
private File file;
private MappedByteBuffer mappedByteBuffer;
private volatile long storeTimestamp = 0;
private boolean firstCreateInQueue = false;
初始化
初始化一个MappedFile对象,使用map函数进行映射。增加文件数量和内存用量。
// 初始化
private void init(final String fileName, final int fileSize) throws IOException {
// 赋值
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
ensureDirOK(this.file.getParent());
// filechannel把文件映射到内存中
// 性能体现在map函数中,原理复杂,可自行学习
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
TOTAL_MAPPED_FILES.incrementAndGet();
ok = true;
}
appendMessagesInner追加消息到文件
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
assert messageExt != null;
assert cb != null;
// 写入的位置
int currentPos = this.wrotePosition.get();
if (currentPos < this.fileSize) {
// 这个writeBuffer对象就是最上面定义的,应该是同一个
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
byteBuffer.position(currentPos);
AppendMessageResult result;
//执行doAppend函数执行实际追加操作
if (messageExt instanceof MessageExtBrokerInner) {
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
} else if (messageExt instanceof MessageExtBatch) {
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
} else {
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
// 写的位置增加
this.wrotePosition.addAndGet(result.getWroteBytes());
// 时间记录
this.storeTimestamp = result.getStoreTimestamp();
return result;
}
log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
doAppend函数
doappend有两个函数,一个是单条消息写入,一个批量消息写入。
最终完成的功能是写入了buffer
// 写入的offset就是文件偏移 + 当前buffer的偏移
long wroteOffset = fileFromOffset + byteBuffer.position();
int sysflag = msgInner.getSysFlag();
// born天生的
// 本来就要占的内存,以及存储占用的内存申请
int bornHostLength = (sysflag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4;
int storeHostLength = (sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 : 16 + 4;
ByteBuffer bornHostHolder = ByteBuffer.allocate(bornHostLength);
ByteBuffer storeHostHolder = ByteBuffer.allocate(storeHostLength);
this.resetByteBuffer(storeHostHolder, storeHostLength);
String msgId;
if ((sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0) {
msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(storeHostHolder), wroteOffset);
} else {
msgId = MessageDecoder.createMessageId(this.msgIdV6Memory, msgInner.getStoreHostBytes(storeHostHolder), wroteOffset);
}
// Record ConsumeQueue information
// 记录消费队列的信息省略
// Transaction messages that require special handling
// 根据消息的类型设置offset,可以看到这里对应了事务消息的设计
// Prepared and Rollback message is not consumed, will not enter the
// consumer queuec
case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
queueOffset = 0L;
break;
case MessageSysFlag.TRANSACTION_NOT_TYPE:
case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
default:
break;
// 再往后即拼接message以及构造返回对象
clean函数中的反射和递归
// clean,限定了 directbuffer, 先获取cleaner, 再执行clean
public static void clean(final ByteBuffer buffer) {
if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)
return;
invoke(invoke(viewed(buffer), "cleaner"), "clean");
}
- 如果满足条件不是堆外内存直接返回不用清理。
- 否则,调用clean,这里出现了一个神奇的语句,invoke封装了反射代理,和method.invoke
- view函数,这里主要是与 viewedBuffer函数 和 attachment函数有关,那他到底是做什么的?
view函数
推断【未验证】: 似乎所有DirectBuffer实现类中的所有attachment函数返回的都是null,相当于是一定返回传入的buffer。 那上面的逻辑是做什么的呢。我感觉应该是用于一些包装类,这些类中的viewdBuffer函数返回的就是实际的buffer。 所以总之这个函数的目的是返回真实的DirectBuffer对象
private static Object invoke(final Object target, final String methodName, final Class<?>... args) {
return AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
Method method = method(target, methodName, args);
method.setAccessible(true);
return method.invoke(target);
}
});
}
// 反射,为啥反射? ByteBuffer 有多个实现类
// 找这个attachment方法,和viewedBuffer方法
private static ByteBuffer viewed(ByteBuffer buffer) {
String methodName = "viewedBuffer";
Method[] methods = buffer.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("attachment")) {
methodName = "attachment";
break;
}
}
ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);
if (viewedBuffer == null)
return buffer;
else
return viewed(viewedBuffer);
}
appendMessage写入文件
这个函数很简单,但是用的地方也很多,直接将byte数组写入buffer
public boolean appendMessage(final byte[] data) {
int currentPos = this.wrotePosition.get();
if ((currentPos + data.length) <= this.fileSize) {
this.fileChannel.position(currentPos);
this.fileChannel.write(ByteBuffer.wrap(data));
this.wrotePosition.addAndGet(data.length);
return true;
}
return false;
}
appendMessage和上面那个那么长的追加消息有什么区别呢
答:在上面那个函数中,传参是一个MessageExt,而且缺少其他信息,例如偏移、iddent等。在经过处理后,实际执行的也是一个buffer.put函数。 这里的函数直接就是byte数组,说明其他类调用时需要先处理再调用。
flush函数写入磁盘
其实这个函数没什么好讲,就是判断是否可以刷新,拿锁,执行force,更新标识位置。 MapperedFile继承了ReferenceResource,可以简单学习一下这个hold和release函数
/**
* @return The current flushed position
*/
public int flush(final int flushLeastPages) {
// 写满了或者有东西可写
if (this.isAbleToFlush(flushLeastPages)) {
if (this.hold()) {
int value = getReadPosition();
//We only append data to fileChannel or mappedByteBuffer, never both.
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
this.mappedByteBuffer.force();
}
this.flushedPosition.set(value);
this.release();
} else {
log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
this.flushedPosition.set(getReadPosition());
}
}
return this.getFlushedPosition();
}
hold和release
可用且大于0则获取成功,双重检验,不满足还可回退。
public synchronized boolean hold() {
if (this.isAvailable()) {
if (this.refCount.getAndIncrement() > 0) {
return true;
} else {
this.refCount.getAndDecrement();
}
}
return false;
}
cleanup函数被mapperedfile子类重写了,最终执行上文出现过的clean函数,将cleanupOver设为true
public void release() {
long value = this.refCount.decrementAndGet();
if (value > 0)
return;
synchronized (this) {
this.cleanupOver = this.cleanup(value);
}
}
commit函数和commit0函数
MappedFile提交实际上是将writeBuffer中的数据写进FileChannel中,这里也出现了我们之前看不懂的transientStorePool,根据他的returnBuffer能大概猜出来是一个对象池模式,像数据库连接池、线程池那样,也会有对应的释放、销毁等操作。
- writeBuffer为空不提交
- 执行commit0提交
- 如果此buffer不再使用,将其归还回给Pool
public int commit(final int commitLeastPages) {
if (writeBuffer == null) {
//no need to commit data to file channel, so just regard wrotePosition as committedPosition.
return this.wrotePosition.get();
}
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
commit0(commitLeastPages);
this.release();
} else {
log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
}
}
// All dirty data has been committed to FileChannel.
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
}
return this.committedPosition.get();
}
protected修饰不会给外部调用。
protected void commit0(final int commitLeastPages) {
//获取写的位置
int writePos = this.wrotePosition.get();
// 上次提交的位点
int lastCommittedPosition = this.committedPosition.get();
if (writePos - lastCommittedPosition > commitLeastPages) {
ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
// 执行write后将提交位点更新,writeBuffer不用管,第三步会根据情况处理
this.committedPosition.set(writePos);
}
}
selectMappedBuffer读消息
主要是slice函数,得到一个切片包含了我们需要的内容 这个地方是MapperByteBuffer,也就是mmap,把文件映射到内存,通过pos快速读取到我们需要的数据。
RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。
public SelectMappedBufferResult selectMappedBuffer(int pos, int size) {
// 这里获取的readPosition是有效数据的末尾位置
int readPosition = getReadPosition();
// 表明读的数据范围是正确的
if ((pos + size) <= readPosition) {
if (this.hold()) {
// 操作buffer
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
byteBuffer.position(pos);
// Creates a new byte buffer whose content is a shared subsequence of this buffer's content.
// 上面的英文是jdk原话,是针对slice函数的,看起来是浅拷贝,得到一个新的buffer,浅拷贝不太准确,是对这一段内容共享
ByteBuffer byteBufferNew = byteBuffer.slice();
byteBufferNew.limit(size);
// 返回了文件偏移,存储了内容的buffer,size和文件本身
return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
} else {log.warn();}
} else {log.warn();}
return null;
}
warmMappedFile预热文件,惊掉了我的下巴
Page Cache机制也不是完全无缺点的,当遇到操作系统进行脏页回写、内存回收、内存交换等情况时,就会引起较大的消息读写延迟。对于这些情况,RocketMQ采用了多种优化技术,比如内存预分配、文件预热、mlock系统调用等,以保证在最大限度地发挥Page Cache机制的优点的同时,尽可能地减少消息读写延迟。所以在生产环境部署RocketMq的时候,尽量采用SSD独享磁盘,这样可以最大限度地保证读写性能。
以上来自参考书籍,上面写道多种优化技术,文件预热、内存预分配、mlock系统调用,那如果不出意外,下面这个函数就是文件预热。去掉了try-catch和log语句。
public void warmMappedFile(FlushDiskType type, int pages) {
// 获得当前buffer的一个切片
ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
// 记录从哪个位点开始写入磁盘了
int flush = 0;
for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
// 在循环中不断向buffer中写内容
byteBuffer.put(i, (byte) 0);
// force flush when flush disk type is sync
// 如果同步刷盘还要注意强制进行刷盘写入文件
if (type == FlushDiskType.SYNC_FLUSH) {
if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
flush = i;
mappedByteBuffer.force();
}
}
// prevent gc
// 阻止GC???
if (j % 1000 == 0) {
Thread.sleep(0);
}
}
// force flush when prepare load finished
// 预加载完成时强制刷新
if (type == FlushDiskType.SYNC_FLUSH) {
mappedByteBuffer.force();
}
this.mlock();
}
通过Thread.sleep来防止GC,又涨知识了
我替大家百度了 为什么Thread.sleep(0)可以阻止rocketmq中的gc? -Java 学习之路 (javaroad.cn)
通过调用 Thread.sleep(0) ,您(可能(它是0,因此实现甚至可以忽略))切换上下文,并且可以选择并行GC线程来清理其他引用 . 副作用是您可能更频繁地运行GC - 这可以防止长时间运行的垃圾收集(您每增加1000次迭代就会增加GC运行的几率)
使当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性 . 该线程不会失去任何监视器的所有权 .
mlock是干嘛的,munock是在哪里调用的?
没错,在方法最后一行出现了this.mlock,上文说这也是一个优化,那他是干嘛的,他为啥出现在这里呢?
首先,mlock最终追溯到了本地方法。然后看了看munlock,大概感觉一下好像是上锁和解锁的两个方法。在上锁是还传入了address参数,莫不是可以只锁一个区间?
经过一番查证,大概就是这两个方法可以锁住内存,防止被交换到swap空间,然后madvise是一次性读,防止缺页中断产生。
public void mlock() {
final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
Pointer pointer = new Pointer(address);
{
int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));
}
{
int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED);
}
}
public void munlock() {
final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
Pointer pointer = new Pointer(address);
int ret = LibC.INSTANCE.munlock(pointer, new NativeLong(this.fileSize));
}
但是问题来了,为啥只执行了mlock?munlock在哪执行的?
public void unlockMappedFile(final MappedFile mappedFile) {
this.scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
mappedFile.munlock();
}
}, 6, TimeUnit.SECONDS);
}
??? 居然是一个定时任务,再往上一层,找到了CommitLog的putMessage方法,在很长的逻辑后有了这个解锁操作。看if里的逻辑,应该是在这个地方判断是否有缓存预热,按理说现在都开始写了,也就预热完或者不用预热了吧。这里的前因后果并不明朗,后面再看看。
if (null != unlockMappedFile
&& this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable())
{
this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
}
MappedFileQueue有什么秘密
文件不止一个,有了MappedFile我们可以轻松进行磁盘读写,但是有很多File啊,为此我们需要一个能管理所有file的类。
内存预分配就是在这里完成的
我们看到在构造方法中,需要传入一个service对象参数,点进去一看,好家伙不得了。 这个类继承了一个线程,然后十分简单的注释。表明这个类就是提前创建mappedFile的,这不就是内存预分配吗。
public MappedFileQueue(final String storePath, int mappedFileSize,
AllocateMappedFileService allocateMappedFileService) {
this.storePath = storePath;
this.mappedFileSize = mappedFileSize;
this.allocateMappedFileService = allocateMappedFileService;
}
**
* Create MappedFile in advance
*/
public class AllocateMappedFileService extends ServiceThread {...}
主要逻辑都在mmapOperation里了。
public void run() {
while (!this.isStopped() && this.mmapOperation()) {
}
}
属性一栏
private static final int DELETE_FILES_BATCH_MAX = 10;
private final String storePath;
private final int mappedFileSize;
// 存储MappedFile对象
private final CopyOnWriteArrayList<MappedFile> mappedFiles
= new CopyOnWriteArrayList<MappedFile>();
private final AllocateMappedFileService allocateMappedFileService;
// 现在刷盘的位置,提交位置,存储时间
private long flushedWhere = 0;
private long committedWhere = 0;
private volatile long storeTimestamp = 0;
方法一栏 (可以先跳过、)
checkSelf()(是否连续)
检查mappedFiles是否有问题(是否连续) 顺便看一看这朴实无华的if嵌套,那些设计来设计去的“高级设计”,我看的想打人。
public void checkSelf() {
List<MappedFile> mappedFiles = new ArrayList<>(this.mappedFiles);
if (!mappedFiles.isEmpty()) {
Iterator<MappedFile> iterator = mappedFiles.iterator();
MappedFile pre = null;
while (iterator.hasNext()) {
MappedFile cur = iterator.next();
if (pre != null) {
if (cur.getFileFromOffset() - pre.getFileFromOffset() != this.mappedFileSize) {
LOG_ERROR.error("[BUG]The mappedFile queue's data is damaged, the adjacent mappedFile's offset don't match. pre file {}, cur file {}",
pre.getFileName(), cur.getFileName());
}
}
pre = cur;
}
}
}
getMappedFileByTime() 通过之前timestamp来获取相应mappedfile。
通过之前timestamp来获取相应mappedfile。 这里getLastModifiedTimestamp是MappedFile提供的一个方法 但归根到底是File类提供的一个方法。mappedFile.file.getXXX(); 最终由本地方法实现。
public MappedFile getMappedFileByTime(final long timestamp) {
Object[] mfs = this.copyMappedFiles(0);
if (null == mfs)
return null;
for (int i = 0; i < mfs.length; i++) {
MappedFile mappedFile = (MappedFile) mfs[i];
if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
return mappedFile;
}
}
return (MappedFile) mfs[mfs.length - 1];
}
// 本地方法实现
public native long getLastModifiedTime(File var1);
truncateDirtyFiles() 截断脏数据文件,(重启恢复)
大致看一眼调用了这个方法的外部方法,都是带有“recover”恢复 的。
一直往上找,最后在BrokerController中出现了,也就是说,是在加载数据时调用这个方法的。
public void truncateDirtyFiles(long offset) {
List<MappedFile> willRemoveFiles = new ArrayList<MappedFile>();
for (MappedFile file : this.mappedFiles) {
// 计算文件尾部偏移量的值
long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
if (fileTailOffset > offset) {
// 如果offset处于文件中间,就把几个指针都强制指向当前offset
if (offset >= file.getFileFromOffset()) {
file.setWrotePosition((int) (offset % this.mappedFileSize));
file.setCommittedPosition((int) (offset % this.mappedFileSize));
file.setFlushedPosition((int) (offset % this.mappedFileSize));
} else {
// 那上一个文件从中间“截断了”,后面的文件放入上面的list
file.destroy(1000);
willRemoveFiles.add(file);
}
}
}
// 执行删除
this.deleteExpiredFile(willRemoveFiles);
}
load() 加载文件
这个方法本身逻辑简单,这里省略了很多异常的处理,catch中会返回false。最终导致broker启动失败。
public boolean load() {
File dir = new File(this.storePath);
File[] files = dir.listFiles();
MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
mappedFile.setWrotePosition(this.mappedFileSize);
mappedFile.setFlushedPosition(this.mappedFileSize);
mappedFile.setCommittedPosition(this.mappedFileSize);
this.mappedFiles.add(mappedFile);
return true;
}
getLastMappedFile() 获取最后一个文件
这里的看的很是头疼啊,不知道有没有错,先往下看。
看其他地方的调用,意思大概是先调用第二个,返回为null的话,有的地方直接return null了。
有的地方会调第一个带参的,而且一般参数传的都是0,createOffset也就是0,此时一定进入第三个if中。
public MappedFile getLastMappedFile(final long startOffset) {
// 本类内部实现,
// MappedFile getLastMappedFile(final long startOffset, boolean needCreate);
// 判断是否需要创建新的文件
return getLastMappedFile(startOffset, true);
}
public MappedFile getLastMappedFile() {
MappedFile[] mappedFiles = this.mappedFiles.toArray(new MappedFile[0]);
return mappedFiles.length == 0 ? null : mappedFiles[mappedFiles.length - 1];
}
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
long createOffset = -1;
MappedFile mappedFileLast = getLastMappedFile();
// 一般搭配第二个,这里必为null
if (mappedFileLast == null) {
createOffset = startOffset - (startOffset % this.mappedFileSize);
}
if (mappedFileLast != null && mappedFileLast.isFull()) {
createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
}
// createOffset必为0,needCreat必为true
if (createOffset != -1 && needCreate) {
String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
String nextNextFilePath = this.storePath + File.separator
+ UtilAll.offset2FileName(createOffset + this.mappedFileSize);
MappedFile mappedFile = null;
// 从预分配好的里直接拿
if (this.allocateMappedFileService != null) {
mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
nextNextFilePath, this.mappedFileSize);
} else {
// 自己创建
mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
}
if (mappedFile != null) {
if (this.mappedFiles.isEmpty()) {
mappedFile.setFirstCreateInQueue(true);
}
this.mappedFiles.add(mappedFile);
}
return mappedFile;
}
return mappedFileLast;
}
resetOffset() 这个方法目前没什么用,我也没看明白
public boolean resetOffset(long offset) {
MappedFile mappedFileLast = getLastMappedFile();
if (mappedFileLast != null) {
long lastOffset = mappedFileLast.getFileFromOffset() +
mappedFileLast.getWrotePosition();
long diff = lastOffset - offset; // 最后的位置减去要设置的位置
final int maxDiff = this.mappedFileSize * 2; // 文件大小×2
// 不是很明白,offset和size单位应该是Byte没错,为啥这里是二倍关系呢
if (diff > maxDiff)
return false;
}
ListIterator<MappedFile> iterator = this.mappedFiles.listIterator();
while (iterator.hasPrevious()) {
mappedFileLast = iterator.previous();
if (offset >= mappedFileLast.getFileFromOffset()) {
int where = (int) (offset % mappedFileLast.getFileSize());
mappedFileLast.setFlushedPosition(where);
mappedFileLast.setWrotePosition(where);
mappedFileLast.setCommittedPosition(where);
break;
} else {
iterator.remove();
}
}
return true;
}
deleteExpiredFileByTime() 按时删除过期文件
主要学设计细节的严密性,间隔设置,要不要等buffer清除完成,每次删除数量。
如果是我的话,可能直接 file = null结束了,剩下的看天意。
public int deleteExpiredFileByTime(final long expiredTime, // 过期时间
final int deleteFilesInterval, // 删除间隔
final long intervalForcibly, // 强制间隔
final boolean cleanImmediately, // 是否立即删除
int deleteFileBatchMax) // 每次删除数量 {
if (deleteFileBatchMax == 0) {deleteFileBatchMax = DELETE_FILES_BATCH_MAX;}
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;
// 判断是否应该删除
if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
// 要求buffer清除完成,如果强制则直接删除
if (mappedFile.destroy(intervalForcibly)) {
files.add(mappedFile);
deleteCount++;
if (files.size() >= deleteFileBatchMax) {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;
}
flush和commit
这两个方法在MappedFile中也有,根据参数选择到相应的MappedFile对象,执行相应操作。
public boolean flush(final int flushLeastPages);
public boolean commit(final int commitLeastPages);
findMappedFileByOffset() 查找文件
shutdown()
这里很简单,但是说实话,我并不能很好的理解,在此之前我们已经遇见过了ReferenceResource,hold和release方法,但是显而易见他们的作用在当时我没有理解。
简单来说,referenceResource归根倒底是管理资源是否可用的,release可以控制cleanupOver,shutdown控制available,而其他操作能否正常进行就要看这两个变量满不满足要求,如此已来和上面的也能照应上。
// MappedFileQueue.java
public void shutdown(final long intervalForcibly) {
for (MappedFile mf : this.mappedFiles) {
mf.shutdown(intervalForcibly);
}
}
// ReferenceResource.java
protected final AtomicLong refCount = new AtomicLong(1);
// 只有shutdown可以改为false
protected volatile boolean available = true;
// 只有clean可以改为true
protected volatile boolean cleanupOver = false;
// 最后一次删除时间
private volatile long firstShutdownTimestamp = 0;
public void release() {
long value = this.refCount.decrementAndGet();
if (value > 0)
return;
synchronized (this) {
// 释放时执行删除
this.cleanupOver = this.cleanup(value);
}
}
public void shutdown(final long intervalForcibly) {
if (this.available) {
// 设为了false
this.available = false;
this.firstShutdownTimestamp = System.currentTimeMillis();
this.release();
} else if (this.getRefCount() > 0) {
if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
this.refCount.set(-1000 - this.getRefCount());
this.release();
}
}
}