Java如何保證文件落盤?


本文轉載自Java如何保證文件落盤?

導語

在之前的文章Linux/UNIX編程如何保證文件落盤中,我們聊了從應用到操作系統,我們要如何保證文件落盤,來確保掉電等故障不會導致數據丟失。JDK也封裝了對應的功能,並且為我們做好了跨平台的保證。

JDK中有三種方式可以強制文件數據落盤:

  1. 調用FileDescriptor#sync函數
  2. 調用FileChannel#force函數
  3. 使用RandomAccessFilerws或者rwd模式打開文件

FileDescriptor#sync

FileDescriptor類提供了sync方法,可以用於保證數據保存到持久化存儲設備后返回:

FileOutputStream outputStream = new FileOutputStream("/Users/mazhibin/b.txt");
outputStream.getFD().sync();

可以看一下JDK是如何實現FileDescriptor#sync的:

public native void sync() throws SyncFailedException;
// jdk/src/solaris/native/java/io/FileDescriptor_md.c
JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_sync(JNIEnv *env, jobject this) {
    // 獲取文件描述符
    FD fd = THIS_FD(this);
    // 調用IO_Sync來執行數據同步
    if (IO_Sync(fd) == -1) {
        JNU_ThrowByName(env, "java/io/SyncFailedException", "sync failed");
    }
}

IO_Sync在UNIX系統上的定義就是fsync

// jdk/src/solaris/native/java/io/io_util_md.h
#define IO_Sync fsync

FileChannel#force

之前的文章提到了,操作系統提供了fsync/fdatasync兩個用戶同步數據到持久化設備的系統調用,后者盡可能的會不同步文件元數據,來減少一次磁盤IO,提高性能。但是Java IO的FileDescriptor#sync只是對fsync的封裝,JDK中沒有對於fdatasync的封裝,這是一個特性缺失。

Java NIO對這一點也做了增強,FileChannel類的force方法,支持傳入一個布爾參數metaData,表示是否需要確保文件元數據落盤,如果為true,則調用fsync。如果為false,則調用fdatasync

使用范例:

FileOutputStream outputStream = new FileOutputStream("/Users/mazhibin/b.txt");

// 強制文件數據與元數據落盤
outputStream.getChannel().force(true);

// 強制文件數據落盤,不關心元數據是否落盤
outputStream.getChannel().force(false);

我們來看看其實現:

public class FileChannelImpl extends FileChannel {
    private final FileDispatcher nd;
    private final FileDescriptor fd;
    private final NativeThreadSet threads = new NativeThreadSet(2);

    public final boolean isOpen() {
        return open;
    }

    private void ensureOpen() throws IOException {
        if(!this.isOpen()) {
            throw new ClosedChannelException();
        }
    }

    // 布爾參數metaData用於指定是否需要文件元數據也確保落盤
    public void force(boolean metaData) throws IOException {
        // 確保文件是已經打開的
        ensureOpen();
        int rv = -1;
        int ti = -1;
        try {
            begin();
            ti = threads.add();

            // 再次確保文件是已經打開的
            if (!isOpen())
                return;
            do {
                // 調用FileDispatcher#force
                rv = nd.force(fd, metaData);
            } while ((rv == IOStatus.INTERRUPTED) && isOpen());
        } finally {
            threads.remove(ti);
            end(rv > -1);
            assert IOStatus.check(rv);
        }
    }
}

實現中有許多線程同步相關的代碼,不屬於我們要關注的部分,就不分析了。FileChannel#force調用FileDispatcher#force

FileDispatcher是NIO內部實現用的一個類,封裝了一些文件操作方法,其中包含了刷新文件的方法:

abstract class FileDispatcher extends NativeDispatcher {

    abstract int force(FileDescriptor fd, boolean metaData) throws IOException;

    // ...
}

FileDispatcher#force的實現:

class FileDispatcherImpl extends FileDispatcher
{

    int force(FileDescriptor fd, boolean metaData) throws IOException {
        return force0(fd, metaData);
    }

    static native int force0(FileDescriptor fd, boolean metaData) throws IOException;

    // ...
}

FileDispatcher#force的本地方法實現:

JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,
                                          jobject fdo, jboolean md)
{
    // 獲取文件描述符
    jint fd = fdval(env, fdo);
    int result = 0;

    if (md == JNI_FALSE) {
        // 如果調用者認為不需要同步文件元數據,調用fdatasync
        result = fdatasync(fd);
    } else {
#ifdef _AIX
        /* On AIX, calling fsync on a file descriptor that is opened only for
         * reading results in an error ("EBADF: The FileDescriptor parameter is
         * not a valid file descriptor open for writing.").
         * However, at this point it is not possibly anymore to read the
         * 'writable' attribute of the corresponding file channel so we have to
         * use 'fcntl'.
         */
        int getfl = fcntl(fd, F_GETFL);
        if (getfl >= 0 && (getfl & O_ACCMODE) == O_RDONLY) {
            return 0;
        }
#endif
        // 如果調用者認為需要同步文件元數據,調用fsync
        result = fsync(fd);
    }
    return handle(env, result, "Force failed");
}

可以看出,其實就是簡單的通過metaData參數來區分調用fsyncfdatasync

RandomAccessFile結合rws/rwd模式

RandomAccessFile打開文件支持4中模式:

  • “r” 以只讀方式打開。調用結果對象的任何 write 方法都將導致拋出 IOException。
  • “rw” 打開以便讀取和寫入。如果該文件尚不存在,則嘗試創建該文件。
  • “rws” 打開以便讀取和寫入,對於 “rw”,還要求對文件的內容或元數據的每個更新都同步寫入到底層存儲設備。
  • “rwd” 打開以便讀取和寫入,對於 “rw”,還要求對文件內容的每個更新都同步寫入到底層存儲設備。

其中rws模式會在open文件時傳入O_SYNC標志位。rwd模式會在open文件時傳入O_DSYNC標志位。

具體的源碼分析參考:JDK源碼閱讀-RandomAccessFile

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM