首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

計算機程序的思維邏輯 (60):RandomAccessFile 實戰 —— 手寫一個高性能鍵值數據庫 BasicDB

     編輯:Java 綜合教學

前言:為什麼流不夠用了?

前兩節介紹的 InputStream/OutputStream 和 Reader/Writer 都有兩大先天限制:

  1. 單向性:要麼只讀,要麼只寫,不能同時讀寫。
  2. 順序性:只能從頭讀到尾,不支持隨機訪問(雖有 mark/reset 但極受限)。

RandomAccessFile 打破了這兩大限制:可讀可寫、可隨機跳轉。它更接近操作系統文件 API(seek/read/write),是實現嵌入式數據庫、索引文件、斷點續傳等系統級組件的利器。

RandomAccessFile 核心 API 速覽

構造與模式

// 文件路徑或 File 對象 + 模式
public RandomAccessFile(String name, String mode) throws FileNotFoundException
public RandomAccessFile(File file, String mode) throws FileNotFoundException

模式參數:

  • "r":只讀
  • "rw":讀寫(最常用)
  • "rws":讀寫 + 內容與元數據同步刷盤(最慢,最安全)
  • "rwd":讀寫 + 內容同步刷盤,元數據不同步(折中)

隨機訪問核心:文件指針

// 獲取當前指針位置
public native long getFilePointer() throws IOException;
// 移動指針到指定位置
public native void seek(long pos) throws IOException;
// 獲取文件長度
public native long length() throws IOException;
// 截斷/擴展文件
public native void setLength(long newLength) throws IOException;

DataInput/DataOutput 接口支持

直接讀寫基本類型,無需手動拆裝箱:

readInt() / writeInt(int v)
readLong() / writeLong(long v)
readUTF() / writeUTF(String str)
readDouble() / writeDouble(double v)
readFully(byte[] b) // 保證讀滿,否則拋 EOFException

避坑指南:別用這兩個方法

  • writeBytes(String s):假設 1 byte = 1 char,中文會亂碼。
  • readLine():同理,無編碼概念,棄用。

實戰:從零實現 BasicDB —— 一個嵌入式鍵值存儲

設計目標

  • 接口類似 Map<String, byte[]>:put/get/remove/flush/close
  • 數據持久化到磁盤,重啟不丟失
  • 內存只存索引(鍵→文件偏移量)與空洞隊列,值數據留在磁盤,支持超大數據集
  • 利用 seek 實現 O(1) 定位讀寫
  • 固定長度槽位存儲值(默認 1020 字節),簡化空間管理

文件結構

  • name.data:值數據文件,每條記錄固定 1024 字節(4 字節長度 + 1020 字節數據 + 填充)
  • name.meta:元數據文件,存儲索引 Map 與空洞隊列,啟動時加載,關閉時持久化

核心數據結構

// 內存索引:鍵 -> 值在 .data 文件中的偏移量
private final Map<String, Long> indexMap = new HashMap<>();
// 空洞隊列:被刪除記錄留下的可復用槽位
private final Queue<Long> gaps = new ArrayDeque<>();
// 數據文件句柄
private RandomAccessFile db;
// 元數據文件
private File metaFile;

// 常量
private static final int MAX_DATA_LENGTH = 1020;
private static final byte[] ZERO_BYTES = new byte[MAX_DATA_LENGTH];
private static final String DATA_SUFFIX = ".data";
private static final String META_SUFFIX = ".meta";

構造方法:啟動或創建

public BasicDB(String path, String name) throws IOException {
    File dataFile = new File(path + name + DATA_SUFFIX);
    metaFile = new File(path + name + META_SUFFIX);
    db = new RandomAccessFile(dataFile, "rw"); // 讀寫模式

    if (metaFile.exists()) {
        loadMeta(); // 現有數據庫:加載索引與空洞
    } else {
        indexMap = new HashMap<>();
        gaps = new ArrayDeque<>();
    }
}

寫入:put(key, value)

public void put(String key, byte[] value) throws IOException {
    Long index = indexMap.get(key);
    if (index == null) { // 新鍵:尋找槽位
        index = nextAvailablePos();
        indexMap.put(key, index);
    }
    writeData(index, value); // 覆寫或新寫
}

private long nextAvailablePos() throws IOException {
    return gaps.isEmpty() ? db.length() : gaps.poll(); // 優先復用空洞
}

private void writeData(long pos, byte[] data) throws IOException {
    if (data.length > MAX_DATA_LENGTH)
        throw new IllegalArgumentException("Value too long: " + data.length);

    db.seek(pos);
    db.writeInt(data.length);           // 4 字節長度
    db.write(data);                     // 實際數據
    db.write(ZERO_BYTES, 0, MAX_DATA_LENGTH - data.length); // 填充到固定槽位
}

讀取:get(key)

public byte[] get(String key) throws IOException {
    Long index = indexMap.get(key);
    return index != null ? getData(index) : null;
}

private byte[] getData(long pos) throws IOException {
    db.seek(pos);
    int length = db.readInt();
    byte[] data = new byte[length];
    db.readFully(data); // 保證讀滿
    return data;
}

刪除:remove(key)

public void remove(String key) {
    Long index = indexMap.remove(key);
    if (index != null) {
        gaps.offer(index); // 標記為可復用,不立即清空磁盤
    }
}

持久化元數據:flush()

public void flush() throws IOException {
    saveMeta();           // 寫入 .meta 文件
    db.getFD().sync();    // 強制數據文件刷盤
}

private void saveMeta() throws IOException {
    try (DataOutputStream out = new DataOutputStream(
            new BufferedOutputStream(new FileOutputStream(metaFile)))) {
        saveIndex(out);
        saveGaps(out);
    }
}

private void saveIndex(DataOutputStream out) throws IOException {
    out.writeInt(indexMap.size());
    for (Map.Entry<String, Long> e : indexMap.entrySet()) {
        out.writeUTF(e.getKey());
        out.writeLong(e.getValue());
    }
}

private void saveGaps(DataOutputStream out) throws IOException {
    out.writeInt(gaps.size());
    for (Long pos : gaps) out.writeLong(pos);
}

加載元數據:loadMeta()

private void loadMeta() throws IOException {
    try (DataInputStream in = new DataInputStream(
            new BufferedInputStream(new FileInputStream(metaFile)))) {
        loadIndex(in);
        loadGaps(in);
    }
}

private void loadIndex(DataInputStream in) throws IOException {
    int size = in.readInt();
    indexMap = new HashMap<>((int)(size / 0.75f) + 1, 0.75f); // 預分配避免擴容
    for (int i = 0; i < size; i++) {
        indexMap.put(in.readUTF(), in.readLong());
    }
}

private void loadGaps(DataInputStream in) throws IOException {
    int size = in.readInt();
    gaps = new ArrayDeque<>(size);
    for (int i = 0; i < size; i++) gaps.add(in.readLong());
}

關閉:close()

public void close() throws IOException {
    flush();
    db.close();
}

使用範例:存儲學生對象

// 對象 -> byte[] (可用序列化、Protobuf、自定義編碼)
private static byte[] toBytes(Student s) throws IOException {
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    DataOutputStream dout = new DataOutputStream(bout);
    dout.writeUTF(s.getName());
    dout.writeInt(s.getAge());
    dout.writeDouble(s.getScore());
    return bout.toByteArray();
}

// 批量寫入
public static void saveStudents(Map<String, Student> students) throws IOException {
    BasicDB db = new BasicDB("./", "students");
    for (Map.Entry<String, Student> e : students.entrySet()) {
        db.put(e.getKey(), toBytes(e.getValue()));
    }
    db.close(); // 自動 flush
}

BasicDB 特性總結與局限

特性表現
內存佔用極低(僅索引與空洞,值不進內存)
讀寫延遲O(1) 隨機訪問,極快
存儲上限受限於文件系統單文件大小,單值 ≤ 1020 字節(可調)
並發安全無(單線程設計,多線程需外部加鎖)
崩潰恢復無 WAL,flush() 前掉電會丟數據
範圍查詢不支持(Hash 索引,無序)

啟發與延伸

  • LSM Tree:將隨機寫轉為順序寫,配合 MemTable + SSTable,是 RocksDB/LevelDB 核心。
  • B+ Tree:支持範圍查詢,MySQL InnoDB、SQLite 採用。
  • MappedByteBuffer:下一節將介紹的內存映射文件,可進一步消除 seek 系統調用開銷。
  • 序列化框架:引入 Protobuf/Kryo 替代手工 DataOutputStream,簡化代碼並提升編解碼性能。

完整代碼獲取

GitHub:https://github.com/swiftma/program-logic

下節預告

訪問文件還有一種方式——內存映射文件。它有什麼特點?何時比 RandomAccessFile 更優?讓我們繼續探索。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved