提交 337f0a4f authored 作者: Thomas Mueller's avatar Thomas Mueller

MVStore: support for concurrent reads and writes is now enabled by default.

上级 a17b66d1
...@@ -8,7 +8,6 @@ package org.h2.mvstore; ...@@ -8,7 +8,6 @@ package org.h2.mvstore;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
/** /**
* A very simple array list that supports concurrent access. * A very simple array list that supports concurrent access.
* Internally, it uses immutable objects. * Internally, it uses immutable objects.
......
...@@ -21,6 +21,15 @@ import org.h2.util.New; ...@@ -21,6 +21,15 @@ import org.h2.util.New;
/** /**
* A stored map. * A stored map.
* <p>
* Read operations can happen concurrently with all other
* operations, without risk of corruption.
* <p>
* Write operations first read the relevant area from disk to memory
* concurrently, and only then modify the data. The in-memory part of write
* operations is synchronized. For scalable concurrent in-memory write
* operations, the map should be split into multiple smaller sub-maps that are
* then synchronized independently.
* *
* @param <K> the key class * @param <K> the key class
* @param <V> the value class * @param <V> the value class
...@@ -43,11 +52,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -43,11 +52,6 @@ public class MVMap<K, V> extends AbstractMap<K, V>
*/ */
protected volatile long writeVersion; protected volatile long writeVersion;
/**
* This version is set during a write operation.
*/
protected volatile long currentWriteVersion = -1;
private int id; private int id;
private long createVersion; private long createVersion;
private final DataType keyType; private final DataType keyType;
...@@ -99,21 +103,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -99,21 +103,6 @@ public class MVMap<K, V> extends AbstractMap<K, V>
this.writeVersion = store.getCurrentVersion(); this.writeVersion = store.getCurrentVersion();
} }
/**
* Create a copy of a page, if the write version is higher than the current
* version. If a copy is created, the old page is marked as deleted.
*
* @param p the page
* @param writeVersion the write version
* @return a page with the given write version
*/
protected Page copyOnWrite(Page p, long writeVersion) {
if (p.getVersion() == writeVersion) {
return p;
}
return p.copy(writeVersion);
}
/** /**
* Add or replace a key-value pair. * Add or replace a key-value pair.
* *
...@@ -126,16 +115,12 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -126,16 +115,12 @@ public class MVMap<K, V> extends AbstractMap<K, V>
public synchronized V put(K key, V value) { public synchronized V put(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null"); DataUtils.checkArgument(value != null, "The value may not be null");
beforeWrite(); beforeWrite();
try { long v = writeVersion;
long v = writeVersion; Page p = root.copy(v);
Page p = copyOnWrite(root, v); p = splitRootIfNeeded(p, v);
p = splitRootIfNeeded(p, v); Object result = put(p, v, key, value);
Object result = put(p, v, key, value); newRoot(p);
newRoot(p); return (V) result;
return (V) result;
} finally {
afterWrite();
}
} }
/** /**
...@@ -155,14 +140,13 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -155,14 +140,13 @@ public class MVMap<K, V> extends AbstractMap<K, V>
Page split = p.split(at); Page split = p.split(at);
Object[] keys = { k }; Object[] keys = { k };
Page.PageReference[] children = { Page.PageReference[] children = {
new Page.PageReference(p, p.getPos()), new Page.PageReference(p, p.getPos(), p.getTotalCount()),
new Page.PageReference(split, split.getPos()), new Page.PageReference(split, split.getPos(), split.getTotalCount()),
}; };
long[] counts = { p.getTotalCount(), split.getTotalCount() };
p = Page.create(this, writeVersion, p = Page.create(this, writeVersion,
1, keys, null, keys, null,
2, children, counts, children,
totalCount, 0, 0); totalCount, 0);
return p; return p;
} }
...@@ -191,21 +175,19 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -191,21 +175,19 @@ public class MVMap<K, V> extends AbstractMap<K, V>
} else { } else {
index++; index++;
} }
Page c = copyOnWrite(p.getChildPage(index), writeVersion); Page c = p.getChildPage(index).copy(writeVersion);
if (c.getMemory() > store.getPageSplitSize() && c.getKeyCount() > 1) { if (c.getMemory() > store.getPageSplitSize() && c.getKeyCount() > 1) {
// split on the way down // split on the way down
int at = c.getKeyCount() / 2; int at = c.getKeyCount() / 2;
Object k = c.getKey(at); Object k = c.getKey(at);
Page split = c.split(at); Page split = c.split(at);
p.setChild(index, split); p.setChild(index, split);
p.setCounts(index, split);
p.insertNode(index, k, c); p.insertNode(index, k, c);
// now we are not sure where to add // now we are not sure where to add
return put(p, writeVersion, key, value); return put(p, writeVersion, key, value);
} }
p.setChild(index, c);
Object result = put(c, writeVersion, key, value); Object result = put(c, writeVersion, key, value);
p.setCounts(index, c); p.setChild(index, c);
return result; return result;
} }
...@@ -510,12 +492,8 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -510,12 +492,8 @@ public class MVMap<K, V> extends AbstractMap<K, V>
@Override @Override
public synchronized void clear() { public synchronized void clear() {
beforeWrite(); beforeWrite();
try { root.removeAllRecursive();
root.removeAllRecursive(); newRoot(Page.createEmpty(this, writeVersion));
newRoot(Page.createEmpty(this, writeVersion));
} finally {
afterWrite();
}
} }
/** /**
...@@ -537,22 +515,24 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -537,22 +515,24 @@ public class MVMap<K, V> extends AbstractMap<K, V>
* @return the old value if the key existed, or null otherwise * @return the old value if the key existed, or null otherwise
*/ */
@Override @Override
public synchronized V remove(Object key) { @SuppressWarnings("unchecked")
public V remove(Object key) {
beforeWrite(); beforeWrite();
try { V result = get(key);
long v = writeVersion; if (result == null) {
Page p = copyOnWrite(root, v); return null;
@SuppressWarnings("unchecked") }
V result = (V) remove(p, v, key); long v = writeVersion;
synchronized (this) {
Page p = root.copy(v);
result = (V) remove(p, v, key);
if (!p.isLeaf() && p.getTotalCount() == 0) { if (!p.isLeaf() && p.getTotalCount() == 0) {
p.removePage(); p.removePage();
p = Page.createEmpty(this, p.getVersion()); p = Page.createEmpty(this, p.getVersion());
} }
newRoot(p); newRoot(p);
return result;
} finally {
afterWrite();
} }
return result;
} }
/** /**
...@@ -664,18 +644,16 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -664,18 +644,16 @@ public class MVMap<K, V> extends AbstractMap<K, V>
index++; index++;
} }
Page cOld = p.getChildPage(index); Page cOld = p.getChildPage(index);
Page c = copyOnWrite(cOld, writeVersion); Page c = cOld.copy(writeVersion);
result = remove(c, writeVersion, key); result = remove(c, writeVersion, key);
if (result == null || c.getTotalCount() != 0) { if (result == null || c.getTotalCount() != 0) {
// no change, or // no change, or
// there are more nodes // there are more nodes
p.setChild(index, c); p.setChild(index, c);
p.setCounts(index, c);
} else { } else {
// this child was deleted // this child was deleted
if (p.getKeyCount() == 0) { if (p.getKeyCount() == 0) {
p.setChild(index, c); p.setChild(index, c);
p.setCounts(index, c);
c.removePage(); c.removePage();
} else { } else {
p.remove(index); p.remove(index);
...@@ -975,25 +953,21 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -975,25 +953,21 @@ public class MVMap<K, V> extends AbstractMap<K, V>
*/ */
void rollbackTo(long version) { void rollbackTo(long version) {
beforeWrite(); beforeWrite();
try { if (version <= createVersion) {
if (version <= createVersion) { // the map is removed later
// the map is removed later } else if (root.getVersion() >= version) {
} else if (root.getVersion() >= version) { while (true) {
while (true) { Page last = oldRoots.peekLast();
Page last = oldRoots.peekLast(); if (last == null) {
if (last == null) { break;
break; }
} // slow, but rollback is not a common operation
// slow, but rollback is not a common operation oldRoots.removeLast(last);
oldRoots.removeLast(last); root = last;
root = last; if (root.getVersion() < version) {
if (root.getVersion() < version) { break;
break;
}
} }
} }
} finally {
afterWrite();
} }
} }
...@@ -1056,45 +1030,7 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -1056,45 +1030,7 @@ public class MVMap<K, V> extends AbstractMap<K, V>
throw DataUtils.newUnsupportedOperationException( throw DataUtils.newUnsupportedOperationException(
"This map is read-only"); "This map is read-only");
} }
checkConcurrentWrite();
store.beforeWrite(); store.beforeWrite();
currentWriteVersion = writeVersion;
}
/**
* Check that no write operation is in progress.
*/
protected void checkConcurrentWrite() {
if (currentWriteVersion != -1) {
// try to detect concurrent modification
// on a best-effort basis
throw DataUtils.newConcurrentModificationException(getName());
}
}
/**
* This method is called after writing to the map (whether or not the write
* operation was successful).
*/
protected void afterWrite() {
currentWriteVersion = -1;
}
/**
* If there is a concurrent update to the given version, wait until it is
* finished.
*
* @param version the read version
*/
protected void waitUntilWritten(long version) {
if (readOnly) {
throw DataUtils.newIllegalStateException(
DataUtils.ERROR_INTERNAL,
"Waiting for writes to a read-only map");
}
while (currentWriteVersion == version) {
Thread.yield();
}
} }
@Override @Override
...@@ -1267,7 +1203,6 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -1267,7 +1203,6 @@ public class MVMap<K, V> extends AbstractMap<K, V>
void copyFrom(MVMap<K, V> sourceMap) { void copyFrom(MVMap<K, V> sourceMap) {
beforeWrite(); beforeWrite();
newRoot(copy(sourceMap.root, null)); newRoot(copy(sourceMap.root, null));
afterWrite();
} }
private Page copy(Page source, CursorPos parent) { private Page copy(Page source, CursorPos parent) {
...@@ -1276,11 +1211,10 @@ public class MVMap<K, V> extends AbstractMap<K, V> ...@@ -1276,11 +1211,10 @@ public class MVMap<K, V> extends AbstractMap<K, V>
Page child = target; Page child = target;
for (CursorPos p = parent; p != null; p = p.parent) { for (CursorPos p = parent; p != null; p = p.parent) {
p.page.setChild(p.index, child); p.page.setChild(p.index, child);
p.page = copyOnWrite(p.page, writeVersion); p.page = p.page.copy(writeVersion);
child = p.page; child = p.page;
if (p.parent == null) { if (p.parent == null) {
newRoot(p.page); newRoot(p.page);
afterWrite();
beforeWrite(); beforeWrite();
} }
} }
......
...@@ -9,17 +9,10 @@ import org.h2.mvstore.type.DataType; ...@@ -9,17 +9,10 @@ import org.h2.mvstore.type.DataType;
import org.h2.mvstore.type.ObjectDataType; import org.h2.mvstore.type.ObjectDataType;
/** /**
* A stored map. Read operations can happen concurrently with all other * A class used for backward compatibility.
* operations, without risk of corruption.
* <p>
* Write operations first read the relevant area from disk to memory
* concurrently, and only then modify the data. The in-memory part of write
* operations is synchronized. For scalable concurrent in-memory write
* operations, the map should be split into multiple smaller sub-maps that are
* then synchronized independently.
* *
* @param <K> the key class * @param <K> the key type
* @param <V> the value class * @param <V> the value type
*/ */
public class MVMapConcurrent<K, V> extends MVMap<K, V> { public class MVMapConcurrent<K, V> extends MVMap<K, V> {
...@@ -27,67 +20,6 @@ public class MVMapConcurrent<K, V> extends MVMap<K, V> { ...@@ -27,67 +20,6 @@ public class MVMapConcurrent<K, V> extends MVMap<K, V> {
super(keyType, valueType); super(keyType, valueType);
} }
@Override
protected Page copyOnWrite(Page p, long writeVersion) {
return p.copy(writeVersion);
}
@Override
protected void checkConcurrentWrite() {
// ignore (writes are synchronized)
}
@Override
@SuppressWarnings("unchecked")
public V put(K key, V value) {
beforeWrite();
try {
// even if the result is the same, we still update the value
// (otherwise compact doesn't work)
get(key);
long v = writeVersion;
synchronized (this) {
Page p = copyOnWrite(root, v);
p = splitRootIfNeeded(p, v);
V result = (V) put(p, v, key, value);
newRoot(p);
return result;
}
} finally {
afterWrite();
}
}
@Override
protected void waitUntilWritten(long version) {
// no need to wait
}
@Override
@SuppressWarnings("unchecked")
public V remove(Object key) {
beforeWrite();
try {
V result = get(key);
if (result == null) {
return null;
}
long v = writeVersion;
synchronized (this) {
Page p = copyOnWrite(root, v);
result = (V) remove(p, v, key);
if (!p.isLeaf() && p.getTotalCount() == 0) {
p.removePage();
p = Page.createEmpty(this, p.getVersion());
}
newRoot(p);
}
return result;
} finally {
afterWrite();
}
}
/** /**
* A builder for this class. * A builder for this class.
* *
......
...@@ -190,7 +190,7 @@ public class MVStore { ...@@ -190,7 +190,7 @@ public class MVStore {
* The metadata map. Write access to this map needs to be synchronized on * The metadata map. Write access to this map needs to be synchronized on
* the store. * the store.
*/ */
private MVMapConcurrent<String, String> meta; private MVMap<String, String> meta;
private final ConcurrentHashMap<Integer, MVMap<?, ?>> maps = private final ConcurrentHashMap<Integer, MVMap<?, ?>> maps =
new ConcurrentHashMap<Integer, MVMap<?, ?>>(); new ConcurrentHashMap<Integer, MVMap<?, ?>>();
...@@ -283,7 +283,7 @@ public class MVStore { ...@@ -283,7 +283,7 @@ public class MVStore {
} }
o = config.get("backgroundExceptionHandler"); o = config.get("backgroundExceptionHandler");
this.backgroundExceptionHandler = (UncaughtExceptionHandler) o; this.backgroundExceptionHandler = (UncaughtExceptionHandler) o;
meta = new MVMapConcurrent<String, String>(StringDataType.INSTANCE, meta = new MVMap<String, String>(StringDataType.INSTANCE,
StringDataType.INSTANCE); StringDataType.INSTANCE);
HashMap<String, Object> c = New.hashMap(); HashMap<String, Object> c = New.hashMap();
c.put("id", 0); c.put("id", 0);
...@@ -1010,7 +1010,6 @@ public class MVStore { ...@@ -1010,7 +1010,6 @@ public class MVStore {
continue; continue;
} }
if (v >= 0 && v >= lastStoredVersion) { if (v >= 0 && v >= lastStoredVersion) {
m.waitUntilWritten(storeVersion);
MVMap<?, ?> r = m.openVersion(storeVersion); MVMap<?, ?> r = m.openVersion(storeVersion);
if (r.getRoot().getPos() == 0) { if (r.getRoot().getPos() == 0) {
changed.add(r); changed.add(r);
......
...@@ -29,10 +29,10 @@ import org.h2.mvstore.type.DataType; ...@@ -29,10 +29,10 @@ import org.h2.mvstore.type.DataType;
*/ */
public class Page { public class Page {
private static final int SHARED_KEYS = 1, SHARED_VALUES = 2, /**
SHARED_CHILDREN = 4, SHARED_COUNTS = 8; * An empty object array.
*/
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
private final MVMap<?, ?> map; private final MVMap<?, ?> map;
private long version; private long version;
...@@ -43,26 +43,11 @@ public class Page { ...@@ -43,26 +43,11 @@ public class Page {
*/ */
private long totalCount; private long totalCount;
/**
* The number of keys.
*/
private int keyCount;
/**
* The number of children.
*/
private int childCount;
/** /**
* The last result of a find operation is cached. * The last result of a find operation is cached.
*/ */
private int cachedCompare; private int cachedCompare;
/**
* Which arrays are shared with another version of this page.
*/
private int sharedFlags;
/** /**
* The estimated memory used. * The estimated memory used.
*/ */
...@@ -89,13 +74,6 @@ public class Page { ...@@ -89,13 +74,6 @@ public class Page {
*/ */
private PageReference[] children; private PageReference[] children;
/**
* The descendant count for each child page.
* <p>
* The array might be larger than needed, to avoid frequent re-sizing.
*/
private long[] counts;
/** /**
* Whether the page is an in-memory (not stored, or not yet stored) page, * Whether the page is an in-memory (not stored, or not yet stored) page,
* and it is removed. This is to keep track of pages that concurrently * and it is removed. This is to keep track of pages that concurrently
...@@ -118,9 +96,9 @@ public class Page { ...@@ -118,9 +96,9 @@ public class Page {
*/ */
static Page createEmpty(MVMap<?, ?> map, long version) { static Page createEmpty(MVMap<?, ?> map, long version) {
return create(map, version, return create(map, version,
0, EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY, EMPTY_OBJECT_ARRAY,
0, null, null, null,
0, 0, DataUtils.PAGE_MEMORY); 0, DataUtils.PAGE_MEMORY);
} }
/** /**
...@@ -128,31 +106,22 @@ public class Page { ...@@ -128,31 +106,22 @@ public class Page {
* *
* @param map the map * @param map the map
* @param version the version * @param version the version
* @param keyCount the number of keys
* @param keys the keys * @param keys the keys
* @param values the values * @param values the values
* @param childCount the number of children
* @param children the child page positions * @param children the child page positions
* @param counts the children counts
* @param totalCount the total number of keys * @param totalCount the total number of keys
* @param sharedFlags which arrays are shared
* @param memory the memory used in bytes * @param memory the memory used in bytes
* @return the page * @return the page
*/ */
public static Page create(MVMap<?, ?> map, long version, int keyCount, public static Page create(MVMap<?, ?> map, long version,
Object[] keys, Object[] values, int childCount, PageReference[] children, Object[] keys, Object[] values, PageReference[] children,
long[] counts, long totalCount, long totalCount, int memory) {
int sharedFlags, int memory) {
Page p = new Page(map, version); Page p = new Page(map, version);
// the position is 0 // the position is 0
p.keyCount = keyCount;
p.keys = keys; p.keys = keys;
p.values = values; p.values = values;
p.childCount = childCount;
p.children = children; p.children = children;
p.counts = counts;
p.totalCount = totalCount; p.totalCount = totalCount;
p.sharedFlags = sharedFlags;
if (memory == 0) { if (memory == 0) {
p.recalculateMemory(); p.recalculateMemory();
} else { } else {
...@@ -176,17 +145,10 @@ public class Page { ...@@ -176,17 +145,10 @@ public class Page {
public static Page create(MVMap<?, ?> map, long version, Page source) { public static Page create(MVMap<?, ?> map, long version, Page source) {
Page p = new Page(map, version); Page p = new Page(map, version);
// the position is 0 // the position is 0
p.keyCount = source.keyCount;
p.keys = source.keys; p.keys = source.keys;
if (source.isLeaf()) { p.values = source.values;
p.values = source.values; p.children = source.children;
} else {
p.childCount = source.childCount;
p.children = source.children;
p.counts = source.counts;
}
p.totalCount = source.totalCount; p.totalCount = source.totalCount;
p.sharedFlags = source.sharedFlags;
p.memory = source.memory; p.memory = source.memory;
MVStore store = map.store; MVStore store = map.store;
if (store != null) { if (store != null) {
...@@ -296,7 +258,7 @@ public class Page { ...@@ -296,7 +258,7 @@ public class Page {
* @return the number of keys * @return the number of keys
*/ */
public int getKeyCount() { public int getKeyCount() {
return keyCount; return keys.length;
} }
/** /**
...@@ -327,14 +289,14 @@ public class Page { ...@@ -327,14 +289,14 @@ public class Page {
int chunkId = DataUtils.getPageChunkId(pos); int chunkId = DataUtils.getPageChunkId(pos);
buff.append("chunk: ").append(Long.toHexString(chunkId)).append("\n"); buff.append("chunk: ").append(Long.toHexString(chunkId)).append("\n");
} }
for (int i = 0; i <= keyCount; i++) { for (int i = 0; i <= keys.length; i++) {
if (i > 0) { if (i > 0) {
buff.append(" "); buff.append(" ");
} }
if (children != null) { if (children != null) {
buff.append("[" + Long.toHexString(children[i].pos) + "] "); buff.append("[" + Long.toHexString(children[i].pos) + "] ");
} }
if (i < keyCount) { if (i < keys.length) {
buff.append(keys[i]); buff.append(keys[i]);
if (values != null) { if (values != null) {
buff.append(':'); buff.append(':');
...@@ -353,9 +315,8 @@ public class Page { ...@@ -353,9 +315,8 @@ public class Page {
*/ */
public Page copy(long version) { public Page copy(long version) {
Page newPage = create(map, version, Page newPage = create(map, version,
keyCount, keys, values, keys, values,
childCount, children, counts, totalCount, children, totalCount,
SHARED_KEYS | SHARED_VALUES | SHARED_CHILDREN | SHARED_COUNTS,
getMemory()); getMemory());
// mark the old as deleted // mark the old as deleted
removePage(); removePage();
...@@ -375,7 +336,7 @@ public class Page { ...@@ -375,7 +336,7 @@ public class Page {
* @return the value or null * @return the value or null
*/ */
public int binarySearch(Object key) { public int binarySearch(Object key) {
int low = 0, high = keyCount - 1; int low = 0, high = keys.length - 1;
// the cached index minus one, so that // the cached index minus one, so that
// for the first time (when cachedCompare is 0), // for the first time (when cachedCompare is 0),
// the default value is used // the default value is used
...@@ -400,7 +361,7 @@ public class Page { ...@@ -400,7 +361,7 @@ public class Page {
return -(low + 1); return -(low + 1);
// regular binary search (without caching) // regular binary search (without caching)
// int low = 0, high = keyCount - 1; // int low = 0, high = keys.length - 1;
// while (low <= high) { // while (low <= high) {
// int x = (low + high) >>> 1; // int x = (low + high) >>> 1;
// int compare = map.compare(key, keys[x]); // int compare = map.compare(key, keys[x]);
...@@ -426,39 +387,36 @@ public class Page { ...@@ -426,39 +387,36 @@ public class Page {
} }
private Page splitLeaf(int at) { private Page splitLeaf(int at) {
int a = at, b = keyCount - a; int a = at, b = keys.length - a;
Object[] aKeys = new Object[a]; Object[] aKeys = new Object[a];
Object[] bKeys = new Object[b]; Object[] bKeys = new Object[b];
System.arraycopy(keys, 0, aKeys, 0, a); System.arraycopy(keys, 0, aKeys, 0, a);
System.arraycopy(keys, a, bKeys, 0, b); System.arraycopy(keys, a, bKeys, 0, b);
keys = aKeys; keys = aKeys;
keyCount = a;
Object[] aValues = new Object[a]; Object[] aValues = new Object[a];
Object[] bValues = new Object[b]; Object[] bValues = new Object[b];
bValues = new Object[b]; bValues = new Object[b];
System.arraycopy(values, 0, aValues, 0, a); System.arraycopy(values, 0, aValues, 0, a);
System.arraycopy(values, a, bValues, 0, b); System.arraycopy(values, a, bValues, 0, b);
values = aValues; values = aValues;
sharedFlags &= ~(SHARED_KEYS | SHARED_VALUES);
totalCount = a; totalCount = a;
Page newPage = create(map, version, Page newPage = create(map, version,
b, bKeys, bValues, bKeys, bValues,
0, null, null, null,
bKeys.length, 0, 0); bKeys.length, 0);
recalculateMemory(); recalculateMemory();
newPage.recalculateMemory(); newPage.recalculateMemory();
return newPage; return newPage;
} }
private Page splitNode(int at) { private Page splitNode(int at) {
int a = at, b = keyCount - a; int a = at, b = keys.length - a;
Object[] aKeys = new Object[a]; Object[] aKeys = new Object[a];
Object[] bKeys = new Object[b - 1]; Object[] bKeys = new Object[b - 1];
System.arraycopy(keys, 0, aKeys, 0, a); System.arraycopy(keys, 0, aKeys, 0, a);
System.arraycopy(keys, a + 1, bKeys, 0, b - 1); System.arraycopy(keys, a + 1, bKeys, 0, b - 1);
keys = aKeys; keys = aKeys;
keyCount = a;
PageReference[] aChildren = new PageReference[a + 1]; PageReference[] aChildren = new PageReference[a + 1];
PageReference[] bChildren = new PageReference[b]; PageReference[] bChildren = new PageReference[b];
...@@ -466,27 +424,19 @@ public class Page { ...@@ -466,27 +424,19 @@ public class Page {
System.arraycopy(children, a + 1, bChildren, 0, b); System.arraycopy(children, a + 1, bChildren, 0, b);
children = aChildren; children = aChildren;
long[] aCounts = new long[a + 1];
long[] bCounts = new long[b];
System.arraycopy(counts, 0, aCounts, 0, a + 1);
System.arraycopy(counts, a + 1, bCounts, 0, b);
counts = aCounts;
childCount = a + 1;
sharedFlags &= ~(SHARED_KEYS | SHARED_CHILDREN | SHARED_COUNTS);
long t = 0; long t = 0;
for (long x : aCounts) { for (PageReference x : aChildren) {
t += x; t += x.count;
} }
totalCount = t; totalCount = t;
t = 0; t = 0;
for (long x : bCounts) { for (PageReference x : bChildren) {
t += x; t += x.count;
} }
Page newPage = create(map, version, Page newPage = create(map, version,
b - 1, bKeys, null, bKeys, null,
b, bChildren, bCounts, bChildren,
t, 0, 0); t, 0);
recalculateMemory(); recalculateMemory();
newPage.recalculateMemory(); newPage.recalculateMemory();
return newPage; return newPage;
...@@ -501,10 +451,10 @@ public class Page { ...@@ -501,10 +451,10 @@ public class Page {
if (MVStore.ASSERT) { if (MVStore.ASSERT) {
long check = 0; long check = 0;
if (isLeaf()) { if (isLeaf()) {
check = keyCount; check = keys.length;
} else { } else {
for (long x : counts) { for (PageReference x : children) {
check += x; check += x.count;
} }
} }
if (check != totalCount) { if (check != totalCount) {
...@@ -523,7 +473,7 @@ public class Page { ...@@ -523,7 +473,7 @@ public class Page {
* @return the descendant count * @return the descendant count
*/ */
long getCounts(int index) { long getCounts(int index) {
return counts[index]; return children[index].count;
} }
/** /**
...@@ -534,40 +484,11 @@ public class Page { ...@@ -534,40 +484,11 @@ public class Page {
*/ */
public void setChild(int index, Page c) { public void setChild(int index, Page c) {
if (c != children[index].page || c.getPos() != children[index].pos) { if (c != children[index].page || c.getPos() != children[index].pos) {
if ((sharedFlags & SHARED_CHILDREN) != 0) { long oldCount = children[index].count;
children = Arrays.copyOf(children, children.length); children = Arrays.copyOf(children, children.length);
sharedFlags &= ~SHARED_CHILDREN; PageReference ref = new PageReference(c, c.pos, c.totalCount);
}
PageReference ref = new PageReference(c, c.pos);
children[index] = ref; children[index] = ref;
} totalCount += c.totalCount - oldCount;
}
/**
* Update the (descendant) count for the given child, if there was a change.
*
* @param index the index
* @param c the new child page
*/
public void setCounts(int index, Page c) {
setCounts(index, c.totalCount);
}
/**
* Update the (descendant) count for the given child, if there was a change.
*
* @param index the index
* @param total the new value
*/
private void setCounts(int index, long total) {
if (total != counts[index]) {
if ((sharedFlags & SHARED_COUNTS) != 0) {
counts = Arrays.copyOf(counts, counts.length);
sharedFlags &= ~SHARED_COUNTS;
}
long oldCount = counts[index];
counts[index] = total;
totalCount += counts[index] - oldCount;
} }
} }
...@@ -578,10 +499,7 @@ public class Page { ...@@ -578,10 +499,7 @@ public class Page {
* @param key the new key * @param key the new key
*/ */
public void setKey(int index, Object key) { public void setKey(int index, Object key) {
if ((sharedFlags & SHARED_KEYS) != 0) { keys = Arrays.copyOf(keys, keys.length);
keys = Arrays.copyOf(keys, keys.length);
sharedFlags &= ~SHARED_KEYS;
}
Object old = keys[index]; Object old = keys[index];
DataType keyType = map.getKeyType(); DataType keyType = map.getKeyType();
int mem = keyType.getMemory(key); int mem = keyType.getMemory(key);
...@@ -601,10 +519,7 @@ public class Page { ...@@ -601,10 +519,7 @@ public class Page {
*/ */
public Object setValue(int index, Object value) { public Object setValue(int index, Object value) {
Object old = values[index]; Object old = values[index];
if ((sharedFlags & SHARED_VALUES) != 0) { values = Arrays.copyOf(values, values.length);
values = Arrays.copyOf(values, values.length);
sharedFlags &= ~SHARED_VALUES;
}
DataType valueType = map.getValueType(); DataType valueType = map.getValueType();
addMemory(valueType.getMemory(value) - addMemory(valueType.getMemory(value) -
valueType.getMemory(old)); valueType.getMemory(old));
...@@ -644,26 +559,15 @@ public class Page { ...@@ -644,26 +559,15 @@ public class Page {
* @param value the value * @param value the value
*/ */
public void insertLeaf(int index, Object key, Object value) { public void insertLeaf(int index, Object key, Object value) {
if (((sharedFlags & SHARED_KEYS) == 0) && keys.length > keyCount + 1) { int len = keys.length + 1;
if (index < keyCount) { Object[] newKeys = new Object[len];
System.arraycopy(keys, index, keys, index + 1, DataUtils.copyWithGap(keys, newKeys, len - 1, index);
keyCount - index); keys = newKeys;
System.arraycopy(values, index, values, index + 1, Object[] newValues = new Object[len];
keyCount - index); DataUtils.copyWithGap(values, newValues, len - 1, index);
} values = newValues;
} else {
int len = keyCount + 6;
Object[] newKeys = new Object[len];
DataUtils.copyWithGap(keys, newKeys, keyCount, index);
keys = newKeys;
Object[] newValues = new Object[len];
DataUtils.copyWithGap(values, newValues, keyCount, index);
values = newValues;
}
keys[index] = key; keys[index] = key;
values[index] = value; values[index] = value;
keyCount++;
sharedFlags &= ~(SHARED_KEYS | SHARED_VALUES);
totalCount++; totalCount++;
addMemory(map.getKeyType().getMemory(key) + addMemory(map.getKeyType().getMemory(key) +
map.getValueType().getMemory(value)); map.getValueType().getMemory(value));
...@@ -678,26 +582,18 @@ public class Page { ...@@ -678,26 +582,18 @@ public class Page {
*/ */
public void insertNode(int index, Object key, Page childPage) { public void insertNode(int index, Object key, Page childPage) {
Object[] newKeys = new Object[keyCount + 1]; Object[] newKeys = new Object[keys.length + 1];
DataUtils.copyWithGap(keys, newKeys, keyCount, index); DataUtils.copyWithGap(keys, newKeys, keys.length, index);
newKeys[index] = key; newKeys[index] = key;
keys = newKeys; keys = newKeys;
keyCount++; int childCount = children.length;
PageReference[] newChildren = new PageReference[childCount + 1]; PageReference[] newChildren = new PageReference[childCount + 1];
DataUtils.copyWithGap(children, newChildren, childCount, index); DataUtils.copyWithGap(children, newChildren, childCount, index);
newChildren[index] = new PageReference(childPage, childPage.getPos()); newChildren[index] = new PageReference(
childPage, childPage.getPos(), childPage.totalCount);
children = newChildren; children = newChildren;
long[] newCounts = new long[childCount + 1];
DataUtils.copyWithGap(counts, newCounts, childCount, index);
newCounts[index] = childPage.totalCount;
counts = newCounts;
childCount++;
sharedFlags &= ~(SHARED_KEYS | SHARED_CHILDREN | SHARED_COUNTS);
totalCount += childPage.totalCount; totalCount += childPage.totalCount;
addMemory(map.getKeyType().getMemory(key) + addMemory(map.getKeyType().getMemory(key) +
DataUtils.PAGE_MEMORY_CHILD); DataUtils.PAGE_MEMORY_CHILD);
...@@ -709,58 +605,32 @@ public class Page { ...@@ -709,58 +605,32 @@ public class Page {
* @param index the index * @param index the index
*/ */
public void remove(int index) { public void remove(int index) {
int keyIndex = index >= keyCount ? index - 1 : index; int keyLength = keys.length;
int keyIndex = index >= keyLength ? index - 1 : index;
Object old = keys[keyIndex]; Object old = keys[keyIndex];
addMemory(-map.getKeyType().getMemory(old)); addMemory(-map.getKeyType().getMemory(old));
if ((sharedFlags & SHARED_KEYS) == 0 && Object[] newKeys = new Object[keyLength - 1];
keys.length > keyCount - 4) { DataUtils.copyExcept(keys, newKeys, keyLength, keyIndex);
if (keyIndex < keyCount - 1) { keys = newKeys;
System.arraycopy(keys, keyIndex + 1, keys, keyIndex, keyCount -
keyIndex - 1);
}
keys[keyCount - 1] = null;
} else {
Object[] newKeys = new Object[keyCount - 1];
DataUtils.copyExcept(keys, newKeys, keyCount, keyIndex);
keys = newKeys;
sharedFlags &= ~SHARED_KEYS;
}
if (values != null) { if (values != null) {
old = values[index]; old = values[index];
addMemory(-map.getValueType().getMemory(old)); addMemory(-map.getValueType().getMemory(old));
if ((sharedFlags & SHARED_VALUES) == 0 && Object[] newValues = new Object[keyLength - 1];
values.length > keyCount - 4) { DataUtils.copyExcept(values, newValues, keyLength, index);
if (index < keyCount - 1) { values = newValues;
System.arraycopy(values, index + 1, values, index,
keyCount - index - 1);
}
values[keyCount - 1] = null;
} else {
Object[] newValues = new Object[keyCount - 1];
DataUtils.copyExcept(values, newValues, keyCount, index);
values = newValues;
sharedFlags &= ~SHARED_VALUES;
}
totalCount--; totalCount--;
} }
keyCount--;
if (children != null) { if (children != null) {
addMemory(-DataUtils.PAGE_MEMORY_CHILD); addMemory(-DataUtils.PAGE_MEMORY_CHILD);
long countOffset = counts[index]; long countOffset = children[index].count;
int childCount = children.length;
PageReference[] newChildren = new PageReference[childCount - 1]; PageReference[] newChildren = new PageReference[childCount - 1];
DataUtils.copyExcept(children, newChildren, childCount, index); DataUtils.copyExcept(children, newChildren, childCount, index);
children = newChildren; children = newChildren;
long[] newCounts = new long[childCount - 1];
DataUtils.copyExcept(counts, newCounts, childCount, index);
counts = newCounts;
sharedFlags &= ~(SHARED_CHILDREN | SHARED_COUNTS);
totalCount -= countOffset; totalCount -= countOffset;
childCount--;
} }
} }
...@@ -801,21 +671,19 @@ public class Page { ...@@ -801,21 +671,19 @@ public class Page {
} }
int len = DataUtils.readVarInt(buff); int len = DataUtils.readVarInt(buff);
keys = new Object[len]; keys = new Object[len];
keyCount = len;
int type = buff.get(); int type = buff.get();
boolean node = (type & 1) == DataUtils.PAGE_TYPE_NODE; boolean node = (type & 1) == DataUtils.PAGE_TYPE_NODE;
if (node) { if (node) {
childCount = len + 1;
children = new PageReference[len + 1]; children = new PageReference[len + 1];
long[] p = new long[len + 1];
for (int i = 0; i <= len; i++) { for (int i = 0; i <= len; i++) {
children[i] = new PageReference(null, buff.getLong()); p[i] = buff.getLong();
} }
counts = new long[len + 1];
long total = 0; long total = 0;
for (int i = 0; i <= len; i++) { for (int i = 0; i <= len; i++) {
long s = DataUtils.readVarLong(buff); long s = DataUtils.readVarLong(buff);
total += s; total += s;
counts[i] = s; children[i] = new PageReference(null, p[i], s);
} }
totalCount = total; totalCount = total;
} }
...@@ -855,7 +723,7 @@ public class Page { ...@@ -855,7 +723,7 @@ public class Page {
*/ */
private int write(Chunk chunk, WriteBuffer buff) { private int write(Chunk chunk, WriteBuffer buff) {
int start = buff.position(); int start = buff.position();
int len = keyCount; int len = keys.length;
int type = children != null ? DataUtils.PAGE_TYPE_NODE int type = children != null ? DataUtils.PAGE_TYPE_NODE
: DataUtils.PAGE_TYPE_LEAF; : DataUtils.PAGE_TYPE_LEAF;
buff.putInt(0). buff.putInt(0).
...@@ -867,7 +735,7 @@ public class Page { ...@@ -867,7 +735,7 @@ public class Page {
if (type == DataUtils.PAGE_TYPE_NODE) { if (type == DataUtils.PAGE_TYPE_NODE) {
writeChildren(buff); writeChildren(buff);
for (int i = 0; i <= len; i++) { for (int i = 0; i <= len; i++) {
buff.putVarLong(counts[i]); buff.putVarLong(children[i].count);
} }
} }
int compressStart = buff.position(); int compressStart = buff.position();
...@@ -931,7 +799,7 @@ public class Page { ...@@ -931,7 +799,7 @@ public class Page {
} }
private void writeChildren(WriteBuffer buff) { private void writeChildren(WriteBuffer buff) {
int len = keyCount; int len = keys.length;
for (int i = 0; i <= len; i++) { for (int i = 0; i <= len; i++) {
buff.putLong(children[i].pos); buff.putLong(children[i].pos);
} }
...@@ -951,12 +819,12 @@ public class Page { ...@@ -951,12 +819,12 @@ public class Page {
} }
int patch = write(chunk, buff); int patch = write(chunk, buff);
if (!isLeaf()) { if (!isLeaf()) {
int len = childCount; int len = children.length;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
Page p = children[i].page; Page p = children[i].page;
if (p != null) { if (p != null) {
p.writeUnsavedRecursive(chunk, buff); p.writeUnsavedRecursive(chunk, buff);
children[i] = new PageReference(p, p.getPos()); children[i] = new PageReference(p, p.getPos(), p.totalCount);
} }
} }
int old = buff.position(); int old = buff.position();
...@@ -971,7 +839,7 @@ public class Page { ...@@ -971,7 +839,7 @@ public class Page {
*/ */
void writeEnd() { void writeEnd() {
if (!isLeaf()) { if (!isLeaf()) {
int len = childCount; int len = children.length;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
PageReference ref = children[i]; PageReference ref = children[i];
if (ref.page != null) { if (ref.page != null) {
...@@ -980,7 +848,7 @@ public class Page { ...@@ -980,7 +848,7 @@ public class Page {
DataUtils.ERROR_INTERNAL, "Page not written"); DataUtils.ERROR_INTERNAL, "Page not written");
} }
ref.page.writeEnd(); ref.page.writeEnd();
children[i] = new PageReference(null, ref.pos); children[i] = new PageReference(null, ref.pos, ref.count);
} }
} }
} }
...@@ -991,7 +859,7 @@ public class Page { ...@@ -991,7 +859,7 @@ public class Page {
} }
public int getRawChildPageCount() { public int getRawChildPageCount() {
return childCount; return children.length;
} }
@Override @Override
...@@ -1032,12 +900,12 @@ public class Page { ...@@ -1032,12 +900,12 @@ public class Page {
private void recalculateMemory() { private void recalculateMemory() {
int mem = DataUtils.PAGE_MEMORY; int mem = DataUtils.PAGE_MEMORY;
DataType keyType = map.getKeyType(); DataType keyType = map.getKeyType();
for (int i = 0; i < keyCount; i++) { for (int i = 0; i < keys.length; i++) {
mem += keyType.getMemory(keys[i]); mem += keyType.getMemory(keys[i]);
} }
if (this.isLeaf()) { if (this.isLeaf()) {
DataType valueType = map.getValueType(); DataType valueType = map.getValueType();
for (int i = 0; i < keyCount; i++) { for (int i = 0; i < keys.length; i++) {
mem += valueType.getMemory(values[i]); mem += valueType.getMemory(values[i]);
} }
} else { } else {
...@@ -1076,9 +944,15 @@ public class Page { ...@@ -1076,9 +944,15 @@ public class Page {
*/ */
final Page page; final Page page;
public PageReference(Page page, long pos) { /**
* The descendant count for this child page.
*/
final long count;
public PageReference(Page page, long pos, long count) {
this.page = page; this.page = page;
this.pos = pos; this.pos = pos;
this.count = count;
} }
} }
......
...@@ -121,7 +121,7 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -121,7 +121,7 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
} }
@Override @Override
protected Object remove(Page p, long writeVersion, Object key) { protected synchronized Object remove(Page p, long writeVersion, Object key) {
Object result = null; Object result = null;
if (p.isLeaf()) { if (p.isLeaf()) {
for (int i = 0; i < p.getKeyCount(); i++) { for (int i = 0; i < p.getKeyCount(); i++) {
...@@ -139,11 +139,10 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -139,11 +139,10 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
// this will mark the old page as deleted // this will mark the old page as deleted
// so we need to update the parent in any case // so we need to update the parent in any case
// (otherwise the old page might be deleted again) // (otherwise the old page might be deleted again)
Page c = copyOnWrite(cOld, writeVersion); Page c = cOld.copy(writeVersion);
long oldSize = c.getTotalCount(); long oldSize = c.getTotalCount();
result = remove(c, writeVersion, key); result = remove(c, writeVersion, key);
p.setChild(i, c); p.setChild(i, c);
p.setCounts(i, c);
if (oldSize == c.getTotalCount()) { if (oldSize == c.getTotalCount()) {
continue; continue;
} }
...@@ -190,45 +189,39 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -190,45 +189,39 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
putOrAdd(key, value, true); putOrAdd(key, value, true);
} }
private Object putOrAdd(SpatialKey key, V value, boolean alwaysAdd) { private synchronized Object putOrAdd(SpatialKey key, V value, boolean alwaysAdd) {
beforeWrite(); beforeWrite();
try { long v = writeVersion;
long v = writeVersion; Page p = root.copy(v);
Page p = copyOnWrite(root, v); Object result;
Object result; if (alwaysAdd || get(key) == null) {
if (alwaysAdd || get(key) == null) { if (p.getMemory() > store.getPageSplitSize() &&
if (p.getMemory() > store.getPageSplitSize() && p.getKeyCount() > 3) {
p.getKeyCount() > 1) { // only possible if this is the root, else we would have
// only possible if this is the root, else we would have // split earlier (this requires pageSplitSize is fixed)
// split earlier (this requires pageSplitSize is fixed) long totalCount = p.getTotalCount();
long totalCount = p.getTotalCount(); Page split = split(p, v);
Page split = split(p, v); Object k1 = getBounds(p);
Object k1 = getBounds(p); Object k2 = getBounds(split);
Object k2 = getBounds(split); Object[] keys = { k1, k2 };
Object[] keys = { k1, k2 }; Page.PageReference[] children = {
Page.PageReference[] children = { new Page.PageReference(p, p.getPos(), p.getTotalCount()),
new Page.PageReference(p, p.getPos()), new Page.PageReference(split, split.getPos(), split.getTotalCount()),
new Page.PageReference(split, split.getPos()), new Page.PageReference(null, 0, 0)
new Page.PageReference(null, 0) };
}; p = Page.create(this, v,
long[] counts = { p.getTotalCount(), keys, null,
split.getTotalCount(), 0 }; children,
p = Page.create(this, v, totalCount, 0);
2, keys, null, // now p is a node; continues
3, children, counts,
totalCount, 0, 0);
// now p is a node; continues
}
add(p, v, key, value);
result = null;
} else {
result = set(p, v, key, value);
} }
newRoot(p); add(p, v, key, value);
return result; result = null;
} finally { } else {
afterWrite(); result = set(p, v, key, value);
} }
newRoot(p);
return result;
} }
/** /**
...@@ -252,10 +245,9 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -252,10 +245,9 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
if (contains(p, i, key)) { if (contains(p, i, key)) {
Page c = p.getChildPage(i); Page c = p.getChildPage(i);
if (get(c, key) != null) { if (get(c, key) != null) {
c = copyOnWrite(c, writeVersion); c = c.copy(writeVersion);
Object result = set(c, writeVersion, key, value); Object result = set(c, writeVersion, key, value);
p.setChild(i, c); p.setChild(i, c);
p.setCounts(i, c);
return result; return result;
} }
} }
...@@ -290,14 +282,12 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -290,14 +282,12 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
} }
} }
} }
Page c = copyOnWrite(p.getChildPage(index), writeVersion); Page c = p.getChildPage(index).copy(writeVersion);
if (c.getMemory() > store.getPageSplitSize() && c.getKeyCount() > 1) { if (c.getMemory() > store.getPageSplitSize() && c.getKeyCount() > 4) {
// split on the way down // split on the way down
Page split = split(c, writeVersion); Page split = split(c, writeVersion);
p = copyOnWrite(p, writeVersion);
p.setKey(index, getBounds(c)); p.setKey(index, getBounds(c));
p.setChild(index, c); p.setChild(index, c);
p.setCounts(index, c);
p.insertNode(index, getBounds(split), split); p.insertNode(index, getBounds(split), split);
// now we are not sure where to add // now we are not sure where to add
add(p, writeVersion, key, value); add(p, writeVersion, key, value);
...@@ -308,7 +298,6 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -308,7 +298,6 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
keyType.increaseBounds(bounds, key); keyType.increaseBounds(bounds, key);
p.setKey(index, bounds); p.setKey(index, bounds);
p.setChild(index, c); p.setChild(index, c);
p.setCounts(index, c);
} }
private Page split(Page p, long writeVersion) { private Page split(Page p, long writeVersion) {
...@@ -412,20 +401,17 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> { ...@@ -412,20 +401,17 @@ public class MVRTreeMap<V> extends MVMap<SpatialKey, V> {
private Page newPage(boolean leaf, long writeVersion) { private Page newPage(boolean leaf, long writeVersion) {
Object[] values; Object[] values;
Page.PageReference[] refs; Page.PageReference[] refs;
long[] c;
if (leaf) { if (leaf) {
values = new Object[4]; values = Page.EMPTY_OBJECT_ARRAY;
refs = null; refs = null;
c = null;
} else { } else {
values = null; values = null;
refs = new Page.PageReference[] { refs = new Page.PageReference[] {
new Page.PageReference(null, 0)}; new Page.PageReference(null, 0, 0)};
c = new long[1];
} }
return Page.create(this, writeVersion, return Page.create(this, writeVersion,
0, new Object[4], values, Page.EMPTY_OBJECT_ARRAY, values,
leaf ? 0 : 1, refs, c, 0, 0, 0); refs, 0, 0);
} }
private static void move(Page source, Page target, int sourceIndex) { private static void move(Page source, Page target, int sourceIndex) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论