H2的存储引擎MVStore剖析(5) —— commit操作和close操作

870 阅读13分钟

导言:“MySQL默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显示地开始一个事务,则每个查询都会当做一个事务执行提交操作” ——《高性能Mysql》

一般来说,数据库都是自动提交的。当然,我们也可以将autocommit 设为0(mysql),然后手动进行事务提交。本文将介绍H2数据库的存储引擎MVStore是怎么进行commit操作和close操作,h2的commit操作链路很长,调用了很多方法,强烈建议先看看H2的存储引擎MVStore剖析(1)——MVStore初始化 - 掘金 (juejin.cn),了解了MVStore的文件结构会容易理解一点。另外,源码中有很多涉及多线程的情况,这里先不聚焦在commit的时候如何处理线程竞争的问题,还是把重点放在commit操作本身。

​ 见识浅薄,欢迎讨论。

在阅读h2存储引擎的commit操作过程中,发现整个链路十分的长,调用了很多的方法。经过阅读之后发现,其实重点主要弄懂以下几个问题:

​ 1.什么时候写入 (进行commit)

​ 2.哪些页需要被写入

​ 3.怎么写入

​ 4.写入完之后怎么读出来

要解决这些问题,本质上还是得对MVStore的存储文件结构有充分的认识。只有了解了它的存储结构,才能知道代码为什么要这样运作,从而更好地理解代码逻辑,对B+树在实际数据库中的应用会更加深刻!!!!!!!!!!!!!!!!!!!!!

一、H2存储引擎结构回顾

1.1MVStore总体结构说明

image-20220222215802126.png

MVStore作为h2数据库存储引擎的核心类,里面包含了很多个MVMap。在h2数据库中,每个MVMap对应了一个数据库中的表,可以用以下方法来创建一个表:

String fileName = "fineName";
MVStore s = MVStore.open(fileName);
MVMap<Integer, String> map = s.openMap("data");

其中data就是对应的表名。MVMap中的key就是表的主键属性,value就是存储的行数据

另外,layout和meta都是特殊的数据库元数据表。layout负责记录所有chunk的信息、每个MVMap的根Page的位置信息、meta表的id。其中一个例子如下:

主键keys:

image-20220222221638688.png

值values:

image-20220222221709463.png

比如第1个key“chunk.1” 对应了values中的第一个元素的信息,关于chunk的值的信息可参考另外一篇博文:H2的存储引擎MVStore剖析(1)——MVStore初始化 - 掘金 (juejin.cn)。第10个key“meta.id” 对应1,表示meta这个MVMap的id是1. 接着可以去layout中查找:root.1 这个key对应的值:38000005242【注:MVStore会给每个MVMap分配一个id,然后字符串“root.id”会作为layout的key记录相应的MVMap的根page的位置值】

image-20220222222207396.png

meta这个MVMap记录了name -> id 和id -> name 的信息。如下面截图中的meta表示MVStore此时有两个MVMap,分别是data和data111

image-20220222222901145.png

1.2h2数据库存储文件结构

image-20220222220742023.png

在commit的时候,MVStore将会通过java.nio.channels.FIleChannel 的write(ByteBuffer src, long position)方法将ByteBuffer中的内容写入存储文件中,其中存储文件是以Block为基本单位的,每个Block的默认大小是4KB。另外,MVStore的commit粒度是chunk,也就是说每commit一次都会有一个chunk(可看看上面图片中MVStore的chunk.1,chunk.2,chunk.3......),每个chunk的大小是N * Block。 每个chunk会占用1~N个Block。

1.3h2数据库存储文件——chunk和Page的构造

对于每一个chunk,它的头和为分别存储了chunk的header和footer。header和footer的格式大概如下:

chunk:1,block:2,len:1,map:6,max:1c0,next:3,pages:2,root:4000004f8c,time:1fc,version:1
chunk:1,block:2,version:1,fletcher:aed9a4f6

image-20220222230647786.png

image-20220222230028981.png

在每一个chunk中,都会有1~多个page,需要说明的是每个page的写入是以字节的形式来写入的,而chunk header,chunk footer,存储文件头都是字符串键值对。我个人理解这是很自然的,因为大量的数据都是存储在page中的,这部分的内容需要以字节来写入从而节省空间。

结合上图解释一下:

其实这里就是一个经典的B+树结构了,每个Page分为LeafPage和NonLeafPage,其中非叶子节点只存储了keys而没有values,叶子节点包含了keys和values。结合代码解释下就是

class Page<K,V> implements Cloneable {
   /**
     * The keys.
     */
    private K[] keys;
}

private static class Leaf<K,V> extends Page<K,V> {
 /**
         * The storage for values.
         */
        private V[] values;

}

    private static class NonLeaf<K,V> extends Page<K,V> {
        /**
         * The child page references.
         */
        private PageReference<K,V>[] children;
        
        }

说白了就是每个page中都有keys,但叶子Page还有values数组。非叶子节点还有指向子节点的数组PageReference<K,V>[] children

另外,可参考前面的博客H2的存储引擎MVStore剖析(2) —— Page的读入 - 掘金 (juejin.cn)。有介绍page写入到存储文件的时候的构造,写入的时候都是以字节的形式

image-20220222230206717.png

至此,对MVStore的所有核心元素:MVMap,Chunk,Page这里都介绍了。在这一节的基础上,继续围绕我们的4个问题,来理解MVStore的commit操作。

二、什么时候写入 (进行commit)

一般来说,手动commit可按如下方法来进行:


String fileName = "filename.mv.db";
FileUtils.delete(fileName);

MVStore s = MVStore.open(fileName);
MVMap<Integer, String> map = s.openMap("data");
    for (int i = 0; i < 100; i++) {
        map.put(i, i + "");
    }
s.commit(); // 手动提交
s.close();

MVStore.open()方法中,会调用MVStore的构造方法,然后设置自动commit。

// setAutoCommitDelay starts the thread, but only if
// the parameter is different from the old value
int delay = DataUtils.getConfigParam(config, "autoCommitDelay", 1000);
setAutoCommitDelay(delay);

2.1setAutoCommitDelay(long delay)方法

设置自动commit的最大延迟时间。 如果不设置自动提交,可以把millis设为0.在这种情况下只有显示地调用commit才会提交。默然是1000ms


public void setAutoCommitDelay(int millis) {
    if (autoCommitDelay == millis) {
        return;
    }
    autoCommitDelay = millis;
    if (fileStore == null || fileStore.isReadOnly()) {
        return;
    }
    stopBackgroundThread(true);
    // start the background thread if needed
    if (millis > 0 && isOpen()) {
        int sleep = Math.max(1, millis / 10);
        BackgroundWriterThread t =
                new BackgroundWriterThread(this, sleep,
                        fileStore.toString());
        if (backgroundWriterThread.compareAndSet(null, t)) {
            t.start();
            serializationExecutor = createSingleThreadExecutor("H2-serialization");
            bufferSaveExecutor = createSingleThreadExecutor("H2-save");
        }
    }
}

在setAutoCommitDelay方法中,主要做的就是启动了一个BackgroundWriterThread线程。该线程的run()方法:

@Override
public void run() {
    while (store.isBackgroundThread()) {
        synchronized (sync) {
            try {
                sync.wait(sleep);
            } catch (InterruptedException ignore) {
            }
        }
        if (!store.isBackgroundThread()) {
            break;
        }
        store.writeInBackground();
    }
}

不难看出wait(sleep)方法会让BackgroundWriterThread进入time-wait状态,被唤醒后调用MVStore的writeInBackground()方法。

方法比较长,还包含了压缩的逻辑。但是主要是下面的部分:

void writeInBackground() {
    try {
       .....
        long time = getTimeSinceCreation();  
        // 如果当前时间大于上次commit的时间加上前面设置的autoCommitDelay,则进行tryCommit()
        if (time > lastCommitTime + autoCommitDelay) {
            tryCommit();
        }
      .....
}

这里就不细将tryCommit 方法了,后面会展开介绍这部分逻辑。

三、哪些页需要被写入

在第一部分有介绍,每一次commit都是以一个chunk为粒度来进行写入的

下面是commit的方法

public long commit() {
    return commit(x -> true);
}

private long commit(Predicate<MVStore> check) {
    // we need to prevent re-entrance, which may be possible,
    // because meta map is modified within storeNow() and that
    // causes beforeWrite() call with possibility of going back here
    if(!storeLock.isHeldByCurrentThread() || currentStoreVersion < 0) {
        storeLock.lock();
        try {
            if (check.test(this)) {
                store(true);
            }
        } finally {
            unlockAndCheckPanicCondition();
        }
    }
    return currentVersion;
}

不难发现重点就是store()方法

3.1store()方法

private void store(boolean syncWrite) {
    assert storeLock.isHeldByCurrentThread();
    assert !saveChunkLock.isHeldByCurrentThread();
    if (isOpenOrStopping()) {
        if (hasUnsavedChanges()) {
            dropUnusedChunks();
            try {
                currentStoreVersion = currentVersion;
                if (fileStore == null) {
                    //noinspection NonAtomicOperationOnVolatileField
                    ++currentVersion;
                    setWriteVersion(currentVersion);
                    metaChanged = false;
                } else {
                    if (fileStore.isReadOnly()) {
                        throw DataUtils.newMVStoreException(
                                DataUtils.ERROR_WRITING_FAILED, "This store is read-only");
                    }
                    storeNow(syncWrite, 0, () -> reuseSpace ? 0 : getAfterLastBlock());
                }
            } finally {
                // in any case reset the current store version,
                // to allow closing the store
                currentStoreVersion = -1;
            }
        }
    }
}

3.2hasUnsavedChanges()方法

注意方法hasUnsavedChanges(),顾名思义,这个方法会判断MVStore是否有未保存的变化。其实现如下:

public boolean hasUnsavedChanges() {
    if (metaChanged) {
        return true;
    }
    long lastStoredVersion = currentVersion - 1;
    for (MVMap<?, ?> m : maps.values()) {
        if (!m.isClosed()) {
            if(m.hasChangesSince(lastStoredVersion)) {
                return true;
            }
        }
    }
    return layout.hasChangesSince(lastStoredVersion) && lastStoredVersion > INITIAL_VERSION;
}

从源码中发现,一共检查了3个方面,第一是meta MVMap,第二是所有的MVMap,第三是layout MVMap。正好就是1.1MVStore总体结构说明里的3种MVMap。

那么,MVMap是怎么判断是否有改变的呢?

这里就需要用MVMap的hasChangesSince()方法来判断

final boolean hasChangesSince(long version) {
    return getRoot().hasChangesSince(version, isPersistent());
}

最终调用的是RootReference的hasChangesSince(long version, boolean persistent)方法

boolean hasChangesSince(long version, boolean persistent) {
    return persistent && (root.isSaved() ? getAppendCounter() > 0 : getTotalCount() > 0)
            || getVersion() > version;
}

这里重点介绍下Page类的isSave()方法

public final boolean isSaved() {
    return DataUtils.isPageSaved(pos);
}

 public static boolean  isPageSaved(long pos) {
        return (pos & ~1L) != 0;
    }

方法很简单。这里需要理解每一个Page的pos值的含义。可参考前面的博文:H2的存储引擎MVStore剖析(2) —— Page的读入 - 掘金 (juejin.cn)

image-20220223010248168.png

因此,isPageSave()方法的逻辑就是当表示某个page的位置的pos值除了最后一个bit之外全都为0的话,那就表示这个page没有保存!

回到hasChangesSince(long version, boolean persistent)的讲解,还有一个条件getVersion() > version。

long getVersion() {
    RootReference<K,V> prev = previous;
    return prev == null || prev.root != root ||
            prev.appendCounter != appendCounter ?
                version : prev.getVersion();
}

表示当前的根page的版本如果大于MVStore的版本,那么就表示有变化。

因此总结下就是:如果 1.这个MVMap的根Page的版本大于MVStore的版本 2.条件(root.isSaved() ? getAppendCounter() > 0 : getTotalCount() > 0)满足【后面会有解释】。

则表示这个MVMap有变化。

判断完当前的MVStore是否有变化后,则会调用storeNow(boolean syncWrite, long reservedLow, Supplier<Long> reservedHighSupplier)方法

3.3storeNow()方法

private void storeNow(boolean syncWrite, long reservedLow, Supplier<Long> reservedHighSupplier) {
    try {
        lastCommitTime = getTimeSinceCreation();
        int currentUnsavedPageCount = unsavedMemory;
        // 线程已经通过storeLock排除了竞争,因此可以用非原子操作来对currentVersion加1
        long version = ++currentVersion;
        ArrayList<Page<?,?>> changed = collectChangedMapRoots(version);

        assert storeLock.isHeldByCurrentThread();
        submitOrRun(serializationExecutor,
                () -> serializeAndStore(syncWrite, reservedLow, reservedHighSupplier,
                                        changed, lastCommitTime, version),
                syncWrite);

        // some pages might have been changed in the meantime (in the newest
        // version)
        saveNeeded = false;
        unsavedMemory = Math.max(0, unsavedMemory - currentUnsavedPageCount);
    } catch (MVStoreException e) {
        panic(e);
    } catch (Throwable e) {
        panic(DataUtils.newMVStoreException(DataUtils.ERROR_INTERNAL, "{0}", e.toString(),
                e));
    }
}

注意collectChangedMapRoots()方法,对于回答我们本节的问题:哪些页需要被写入? 非常关键。

3.4ArrayList<Page> collectChangedMapRoots(long version)方法

主要源码:

private ArrayList<Page<?,?>> collectChangedMapRoots(long version) {
    long lastStoredVersion = version - 2;
    ArrayList<Page<?,?>> changed = new ArrayList<>();
    
    //迭代每一个MVMap
    for (Iterator<MVMap<?, ?>> iter = maps.values().iterator(); iter.hasNext(); ) {
        MVMap<?, ?> map = iter.next();
        RootReference<?,?> rootReference = map.setWriteVersion(version);
        if (rootReference == null) {
            iter.remove();
        } else if (map.getCreateVersion() < version && // if map was created after storing started, skip it
                !map.isVolatile() &&
                map.hasChangesSince(lastStoredVersion)) { // 判断当前的MVMap是否有变化
            assert rootReference.version <= version : rootReference.version + " > " + version;
            Page<?,?> rootPage = rootReference.root;
            if (!rootPage.isSaved() ||       // 再次判断当前MVMap的根Page是否没有被保存
                    // after deletion previously saved leaf
                    // may pop up as a root, but we still need
                    // to save new root pos in meta
                    rootPage.isLeaf()) {
                changed.add(rootPage);
            }
        }
    }
    // 用同样的方法检查meta MVMap
    RootReference<?,?> rootReference = meta.setWriteVersion(version);
    if (meta.hasChangesSince(lastStoredVersion) || metaChanged) {
        assert rootReference != null && rootReference.version <= version
                : rootReference == null ? "null" : rootReference.version + " > " + version;
        Page<?, ?> rootPage = rootReference.root;
        if (!rootPage.isSaved() ||
                // after deletion previously saved leaf
                // may pop up as a root, but we still need
                // to save new root pos in meta
                rootPage.isLeaf()) {
            changed.add(rootPage);
        }
    }
    return changed;
}

因此,此方法最主要的作用就是把所有有变化的MVMap的根Page收集起来。而判断的依据有两个:1.map.hasChangesSince(lastStoredVersion)返回true

2.!rootPage.isSaved() 未保存。

3.5serializeAndStore()方法

继续解释storeNow()方法。

我们发现收集完所有有变化的MVMap的根Page后,会给一个线程池提交一个任务。该线程池运行的任务就是调用serializeAndStore()方法。其代码如下:

private void serializeAndStore(boolean syncRun, long reservedLow, Supplier<Long> reservedHighSupplier,
                                ArrayList<Page<?,?>> changed, long time, long version) {
    serializationLock.lock();
    try {
        // 创建一个Chunk。对应了前面的解释,每一个commit都对应了一个chunk
        Chunk c = createChunk(time, version);
        chunks.put(c.id, c);
        WriteBuffer buff = getWriteBuffer();
        // 将所有有变化的MVMap的根Page写到buffer中
        serializeToBuffer(buff, changed, c, reservedLow, reservedHighSupplier);

        submitOrRun(bufferSaveExecutor, () -> storeBuffer(c, buff, changed), syncRun);

    } catch (MVStoreException e) {
        panic(e);
    } catch (Throwable e) {
        panic(DataUtils.newMVStoreException(DataUtils.ERROR_INTERNAL, "{0}", e.toString(), e));
    } finally {
        serializationLock.unlock();
    }
}

主要的逻辑都在serializeToBuffer方法中了

private void serializeToBuffer(WriteBuffer buff, ArrayList<Page<?, ?>> changed, Chunk c,
                                long reservedLow, Supplier<Long> reservedHighSupplier) {
    // need to patch the header later
    c.writeChunkHeader(buff, 0);
    int headerLength = buff.position() + 44;
    buff.position(headerLength);

    long version = c.version;
    List<Long> toc = new ArrayList<>();
    for (Page<?,?> p : changed) {
        String key = MVMap.getMapRootKey(p.getMapId());
        if (p.getTotalCount() == 0) {
            layout.remove(key);
        } else {
            p.writeUnsavedRecursive(c, buff, toc);
            long root = p.getPos();
            layout.put(key, Long.toHexString(root));
        }
    }

    acceptChunkOccupancyChanges(c.time, version);

    RootReference<String,String> layoutRootReference = layout.setWriteVersion(version);
    assert layoutRootReference != null;
    assert layoutRootReference.version == version : layoutRootReference.version + " != " + version;
    metaChanged = false;

    acceptChunkOccupancyChanges(c.time, version);

    onVersionChange(version);

    Page<String,String> layoutRoot = layoutRootReference.root;
    layoutRoot.writeUnsavedRecursive(c, buff, toc);
    c.layoutRootPos = layoutRoot.getPos();
    changed.add(layoutRoot);

    // last allocated map id should be captured after the meta map was saved, because
    // this will ensure that concurrently created map, which made it into meta before save,
    // will have it's id reflected in mapid field of currently written chunk
    c.mapId = lastMapId.get();

    c.tocPos = buff.position();
    long[] tocArray = new long[toc.size()];
    int index = 0;
    for (long tocElement : toc) {
        tocArray[index++] = tocElement;
        buff.putLong(tocElement);
        if (DataUtils.isLeafPosition(tocElement)) {
            ++leafCount;
        } else {
            ++nonLeafCount;
        }
    }
    chunksToC.put(c.id, tocArray);
    int chunkLength = buff.position();

    // add the store header and round to the next block
    int length = MathUtils.roundUpInt(chunkLength +
            Chunk.FOOTER_LENGTH, BLOCK_SIZE);
    buff.limit(length);

    saveChunkLock.lock();
    try {
        Long reservedHigh = reservedHighSupplier.get();
        long filePos = fileStore.allocate(buff.limit(), reservedLow, reservedHigh);
        c.len = buff.limit() / BLOCK_SIZE;
        c.block = filePos / BLOCK_SIZE;
        assert validateFileLength(c.asString());
        // calculate and set the likely next position
        if (reservedLow > 0 || reservedHigh == reservedLow) {
            c.next = fileStore.predictAllocation(c.len, 0, 0);
        } else {
            // just after this chunk
            c.next = 0;
        }
        assert c.pageCountLive == c.pageCount : c;
        assert c.occupancy.cardinality() == 0 : c;

        buff.position(0);
        assert c.pageCountLive == c.pageCount : c;
        assert c.occupancy.cardinality() == 0 : c;
        c.writeChunkHeader(buff, headerLength);

        buff.position(buff.limit() - Chunk.FOOTER_LENGTH);
        buff.put(c.getFooterBytes());
    } finally {
        saveChunkLock.unlock();
    }
}

3.5.1 将所有page

for (Page<?,?> p : changed) {
    String key = MVMap.getMapRootKey(p.getMapId());
    if (p.getTotalCount() == 0) {
        layout.remove(key);
    } else {
        p.writeUnsavedRecursive(c, buff, toc);
        long root = p.getPos();
        layout.put(key, Long.toHexString(root));
    }
}

毫无疑问重点就是Page的writeUnsavedRecursive(c, buff, toc)方法。

其中叶子节点的实现如下:

void writeUnsavedRecursive(Chunk chunk, WriteBuffer buff, List<Long> toc) {
    if (!isSaved()) {
        write(chunk, buff, toc);
    }
}

非叶子节点实现如下:

void writeUnsavedRecursive(Chunk chunk, WriteBuffer buff, List<Long> toc) {
    if (!isSaved()) {
        int patch = write(chunk, buff, toc);
        writeChildrenRecursive(chunk, buff, toc);
        int old = buff.position();
        //子page的pos改变了,需要重新定位到children的位置,重新写入新的children的page 位置
        buff.position(patch);
        writeChildren(buff, false);
        buff.position(old);
    }
}

主要就是多了递归的来调用。

其主要逻辑还是write()方法:

protected final int write(Chunk chunk, WriteBuffer buff, List<Long> toc) {
    pageNo = toc.size();
    int start = buff.position();
    int len = getKeyCount();
    int type = isLeaf() ? PAGE_TYPE_LEAF : DataUtils.PAGE_TYPE_NODE;
    buff.putInt(0).         // placeholder for pageLength
        putShort((byte)0).  // placeholder for check
        putVarInt(map.getId()).
        putVarInt(len);
    int typePos = buff.position();
    buff.put((byte) (type | DataUtils.PAGE_HAS_PAGE_NO));
    int childrenPos = buff.position();
    writeChildren(buff, true);
    int compressStart = buff.position();
    map.getKeyType().write(buff, keys, len);
    writeValues(buff);
    MVStore store = map.getStore();
    int expLen = buff.position() - compressStart;
    if (expLen > 16) {
        int compressionLevel = store.getCompressionLevel();
        if (compressionLevel > 0) {
            Compressor compressor;
            int compressType;
            if (compressionLevel == 1) {
                compressor = store.getCompressorFast();
                compressType = DataUtils.PAGE_COMPRESSED;
            } else {
                compressor = store.getCompressorHigh();
                compressType = DataUtils.PAGE_COMPRESSED_HIGH;
            }
            byte[] comp = new byte[expLen * 2];
            ByteBuffer byteBuffer = buff.getBuffer();
            int pos = 0;
            byte[] exp;
            if (byteBuffer.hasArray()) {
                exp = byteBuffer.array();
                pos = byteBuffer.arrayOffset()  + compressStart;
            } else {
                exp = Utils.newBytes(expLen);
                buff.position(compressStart).get(exp);
            }
            int compLen = compressor.compress(exp, pos, expLen, comp, 0);
            int plus = DataUtils.getVarIntLen(compLen - expLen);
            if (compLen + plus < expLen) {
                buff.position(typePos)
                    .put((byte) (type | DataUtils.PAGE_HAS_PAGE_NO | compressType));
                buff.position(compressStart)
                    .putVarInt(expLen - compLen)
                    .put(comp, 0, compLen);
            }
        }
    }
    int pageLength = buff.position() - start;
    if (pageNo >= 0) {
        buff.putVarInt(pageNo);
    }
    long tocElement = DataUtils.getTocElement(getMapId(), start, buff.position() - start, type);
    toc.add(tocElement);
    int chunkId = chunk.id;
    int check = DataUtils.getCheckValue(chunkId)
            ^ DataUtils.getCheckValue(start)
            ^ DataUtils.getCheckValue(pageLength);
    buff.putInt(start, pageLength).
        putShort(start + 4, (short) check);
    if (isSaved()) {
        throw DataUtils.newMVStoreException(
                DataUtils.ERROR_INTERNAL, "Page already stored");
    }
    long pagePos = DataUtils.getPagePos(chunkId, tocElement);
    boolean isDeleted = isRemoved();
    while (!posUpdater.compareAndSet(this, isDeleted ? 1L : 0L, pagePos)) {
        isDeleted = isRemoved();
    }
    store.cachePage(this);
    if (type == DataUtils.PAGE_TYPE_NODE) {
        // cache again - this will make sure nodes stays in the cache
        // for a longer time
        store.cachePage(this);
    }
    int pageLengthEncoded = DataUtils.getPageMaxLength(pos);
    boolean singleWriter = map.isSingleWriter();
    chunk.accountForWrittenPage(pageLengthEncoded, singleWriter);
    if (isDeleted) {
        store.accountForRemovedPage(pagePos, chunk.version + 1, singleWriter, pageNo);
    }
    diskSpaceUsed = pageLengthEncoded != DataUtils.PAGE_LARGE ? pageLengthEncoded : pageLength;
    return childrenPos;
}

当然,这里的实现看上去虽然代码很长,其实这里的实现是按照前面说的Page的格式来写入的。仔细看代码并不难理解。

image-20220222230206717.png

有一点需要提醒下就是在非叶子节点写入那里:

void writeUnsavedRecursive(Chunk chunk, WriteBuffer buff, List<Long> toc) {
    if (!isSaved()) {
        int patch = write(chunk, buff, toc);
        writeChildrenRecursive(chunk, buff, toc);
        int old = buff.position();
        //子page的pos改变了,需要重新定位到children的位置,重新写入新的children的page 位置
        buff.position(patch);
        writeChildren(buff, false);
        buff.position(old);
    }
}

因为是先把父page写入,因此在write方法中调用writeChildren方法来记录children的位置的时候其实新的children的page还没写入。

image-20220223022031668.png

因此就先把patch记录下来,等递归调用完子page的写入之后再把新的子page的位置写入。

3.6结合put操作讲解哪些页需要写入

再次看叶子节点和非叶子节点的写入实现:

void writeUnsavedRecursive(Chunk chunk, WriteBuffer buff, List<Long> toc) {
    if (!isSaved()) {
        write(chunk, buff, toc);
    }
}
void writeUnsavedRecursive(Chunk chunk, WriteBuffer buff, List<Long> toc) {
    if (!isSaved()) {
        int patch = write(chunk, buff, toc);
        writeChildrenRecursive(chunk, buff, toc);
        int old = buff.position();
        //子page的pos改变了,需要重新定位到children的位置,重新写入新的children的page 位置
        buff.position(patch);
        writeChildren(buff, false);
        buff.position(old);
    }
}

可以看到,每次写入这个page的时候,都需要判断下是否已经saved,判断的逻辑前面有讲到。

public final boolean isSaved() {
    return DataUtils.isPageSaved(pos);
}

在这里,会结合一个具体的例子。介绍下哪些page满足!isSaved()。我们分别执行如下代码:

代码1:

String fileName = "fileName.mv.db";
FileUtils.delete(fileName);

MVStore s = MVStore.open(fileName);
MVMap<Integer, String> map = s.openMap("data111");

System.out.println(map.get(10));
for (int i = 0; i < 100; i++) {
    map.put(i, i + "");
}
s.commit();
s.close();

代码2:

String fileName = "fileName.mv.db";
MVStore s = MVStore.open(fileName);
MVMap<Integer, String> map = s.openMap("data");

System.out.println(map.get(10));

map.put(300, "300");

s.commit();
s.close();
System.out.println("close");

我们在代码2的map.put(300, "300");打一个断点,观察这个put操作的过程。

较为详细的过程可参考之前专门讲put操作的博文:H2的存储引擎MVStore剖析(4) —— PUT操作 - 掘金 (juejin.cn)

这里只针对部分重要代码:

CursorPos<K,V> pos = CursorPos.traverseDown(rootPage, key);
if(!locked && rootReference != getRoot()) {
    continue;
}
Page<K,V> p = pos.page;
int index = pos.index;
tip = pos;
pos = pos.parent;
result = index < 0 ? null : p.getValue(index);
Decision decision = decisionMaker.decide(result, value, tip);

.....

   case PUT: {
                        value = decisionMaker.selectValue(result, value);
                        p = p.copy();
                        if (index < 0) {
                            p.insertLeaf(-index - 1, key, value);
                            int keyCount;
                            while ((keyCount = p.getKeyCount()) > store.getKeysPerPage()
                                    || p.getMemory() > store.getMaxPageSize()
                                    && keyCount > (p.isLeaf() ? 1 : 2)) {
                                long totalCount = p.getTotalCount();
                                int at = keyCount >> 1;
                                K k = p.getKey(at);
                                Page<K,V> split = p.split(at);
                                unsavedMemoryHolder.value += p.getMemory() + split.getMemory();
                                if (pos == null) {
                                    K[] keys = p.createKeyStorage(1);
                                    keys[0] = k;
                                    Page.PageReference<K,V>[] children = Page.createRefStorage(2);
                                    children[0] = new Page.PageReference<>(p);
                                    children[1] = new Page.PageReference<>(split);
                                    p = Page.createNode(this, keys, children, totalCount, 0);
                                    break;
                                }
                                Page<K,V> c = p;
                                p = pos.page;
                                index = pos.index;
                                pos = pos.parent;
                                p = p.copy();
                                p.setChild(index, split);
                                p.insertNode(index, k, c);
                            }
                        } else {
                            p.setValue(index, value);
                        }
                        break;
                    }
                    
                    .....
                     rootPage = replacePage(pos, p, unsavedMemoryHolder);

首先:CursorPos<K,V> pos = CursorPos.traverseDown(rootPage, key);会从根Page开始查找当前的key。它的查找是一个递归读入Page的过程,

static <K,V> CursorPos<K,V> traverseDown(Page<K,V> page, K key) {
    CursorPos<K,V> cursorPos = null;
    while (!page.isLeaf()) {
        int index = page.binarySearch(key) + 1;
        if (index < 0) {
            index = -index;
        }
        cursorPos = new CursorPos<>(page, index, cursorPos);
        page = page.getChildPage(index);
    }
    return new CursorPos<>(page, page.binarySearch(key), cursorPos);
}

最后形成的结果是一个从根到叶子的链路,如图所示:

image-20220223024753269.png

会形成4个CursorPos,并且能通过parent找到上一层的父CursorPos。

在这里,page调用copy方法。会返回一个除了post=0之外,其它值都没变的副本。 可以看下面debug过程中的截图:

image-20220223025147727.png

image-20220223025246782.png

image-20220223025501750.png

另外,在put操作之后的replacePage方法中,会把CursorPos整条从底到上的链路都copy一次,因此从最底层的叶子节点到根节点的pos都变为0了。

代码逻辑不难,如下:

private static <K,V> Page<K,V> replacePage(CursorPos<K,V> path, Page<K,V> replacement,
        IntValueHolder unsavedMemoryHolder) {
    int unsavedMemory = replacement.isSaved() ? 0 : replacement.getMemory();
    while (path != null) {
        Page<K,V> parent = path.page;
        // condition below should always be true, but older versions (up to 1.4.197)
        // may create single-childed (with no keys) internal nodes, which we skip here
        if (parent.getKeyCount() > 0) {
            Page<K,V> child = replacement;
            replacement = parent.copy();
            replacement.setChild(path.index, child);
            unsavedMemory += replacement.getMemory();
        }
        path = path.parent;
    }
    unsavedMemoryHolder.value += unsavedMemory;
    return replacement;
}

因此isSaved() 就返回false

public final boolean isSaved() {
    return DataUtils.isPageSaved(pos);
}

 public static boolean  isPageSaved(long pos) {
        return (pos & ~1L) != 0;
    }

总结一下就是:

每一个chunk包含了每个版本的变化:从B+树的最底层的page一直到该MVMap的根page,这中间经过的page都需要被写入

写入前:

image-20210819232721151.png

写入后:

image-20210819234009093.png

总结:

由于篇幅有限,本文回答了开篇提出的4个问题中的前两个。

​ 1.什么时候写入 (进行commit)

手动commit或者通过设置参数autoCommitDelay来控制后台commit线程。

​ 2.哪些页需要被写入

每一个chunk包含了每个版本的变化:从B+树的最底层的page一直到该MVMap的根page,这中间经过的page都需要被写入