导言:“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总体结构说明
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:
值values:
比如第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的位置值】
meta这个MVMap记录了name -> id 和id -> name 的信息。如下面截图中的meta表示MVStore此时有两个MVMap,分别是data和data111
1.2h2数据库存储文件结构
在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
在每一个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写入到存储文件的时候的构造,写入的时候都是以字节的形式
至此,对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)
因此,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的格式来写入的。仔细看代码并不难理解。
有一点需要提醒下就是在非叶子节点写入那里:
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还没写入。
因此就先把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);
}
最后形成的结果是一个从根到叶子的链路,如图所示:
会形成4个CursorPos,并且能通过parent找到上一层的父CursorPos。
在这里,page调用copy方法。会返回一个除了post=0之外,其它值都没变的副本。 可以看下面debug过程中的截图:
另外,在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都需要被写入
写入前:
写入后:
总结:
由于篇幅有限,本文回答了开篇提出的4个问题中的前两个。
1.什么时候写入 (进行commit)
手动commit或者通过设置参数autoCommitDelay来控制后台commit线程。
2.哪些页需要被写入
每一个chunk包含了每个版本的变化:从B+树的最底层的page一直到该MVMap的根page,这中间经过的page都需要被写入