提交 4b0ce7b8 authored 作者: Thomas Mueller's avatar Thomas Mueller

MVStore: support concurrent transactions (two-phase commit)

上级 bf7e2a7a
......@@ -56,7 +56,7 @@ But it can be also directly within an application, without using JDBC or SQL.
</li><li>Both file-based persistence and in-memory operation are supported.
</li><li>It is intended to be fast, simple to use, and small.
</li><li>Old versions of the data can be read concurrently with all other operations.
</li><li>Transaction are supported.
</li><li>Transaction are supported (including concurrent transactions and 2-phase commit).
</li><li>The tool is very modular. It supports pluggable data types / serialization,
pluggable map implementations (B-tree, R-tree, concurrent B-tree currently), BLOB storage,
and a file system abstraction to support encrypted files and zip files.
......@@ -227,6 +227,7 @@ and the entries of a transaction are removed when the transaction is committed).
The storage overhead of this utility is very small compared to the overhead of a regular transaction log.
The tool supports PostgreSQL style "read committed" transaction isolation.
There is no limit on the size of a transaction (the log is not kept in memory).
The tool supports savepoints, two-phase commit, and other features typically available in a database.
</p>
<h3 id="inMemory">In-Memory Performance and Usage</h3>
......
......@@ -44,8 +44,8 @@ H:3,...
TODO:
- rolling docs review: at convert "Features" to top-level (linked) entries
- mvcc with multiple transactions
- test LZ4
- additional test async write / read algorithm for speed and errors
- move setters to the builder, except for setRetainVersion, setReuseSpace,
and settings that are persistent (setStoreVersion)
......
......@@ -6,13 +6,19 @@
*/
package org.h2.mvstore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.h2.util.New;
/**
* A store that supports concurrent transactions.
*/
public class TransactionStore {
private static final String LAST_TRANSACTION_ID = "lastTransactionId";
/**
* The store.
......@@ -20,10 +26,16 @@ public class TransactionStore {
final MVStore store;
/**
* The map of open transaction.
* Key: transactionId, value: baseVersion.
* The persisted map of open transaction.
* Key: transactionId, value: [ status, nameÊ].
*/
final MVMap<Long, Long> openTransactions;
final MVMap<Long, Object[]> openTransactions;
/**
* The map of open transaction objects.
* Key: transactionId, value: transaction object.
*/
final HashMap<Long, Transaction> openTransactionMap = New.hashMap();
/**
* The undo log.
......@@ -41,6 +53,8 @@ public class TransactionStore {
*/
private final MVMap<String, String> settings;
private long lastTransactionIdStored;
private long lastTransactionId;
/**
......@@ -52,33 +66,59 @@ public class TransactionStore {
this.store = store;
settings = store.openMap("settings");
openTransactions = store.openMap("openTransactions",
new MVMapConcurrent.Builder<Long, Long>());
// TODO one undo log per transaction to speed up commit
// (alternative: add a range delete operation for maps)
new MVMapConcurrent.Builder<Long, Object[]>());
// commit could be faster if we have one undo log per transaction,
// or a range delete operation for maps
undoLog = store.openMap("undoLog",
new MVMapConcurrent.Builder<long[], Object[]>());
init();
}
private void init() {
String s = settings.get("lastTransaction");
String s = settings.get(LAST_TRANSACTION_ID);
if (s != null) {
lastTransactionId = Long.parseLong(s);
lastTransactionIdStored = lastTransactionId;
}
Long t = openTransactions.lastKey();
if (t != null) {
if (t.longValue() > lastTransactionId) {
throw DataUtils.newIllegalStateException("Last transaction not stored");
Long lastKey = openTransactions.lastKey();
if (lastKey != null && lastKey.longValue() > lastTransactionId) {
throw DataUtils.newIllegalStateException("Last transaction not stored");
}
Cursor<Long> cursor = openTransactions.keyIterator(null);
while (cursor.hasNext()) {
long id = cursor.next();
Object[] data = openTransactions.get(id);
int status = (Integer) data[0];
String name = (String) data[1];
long[] next = { id + 1, 0 };
long[] last = undoLog.floorKey(next);
if (last == null) {
// no entry
} else if (last[0] == id) {
Transaction t = new Transaction(this, id, status, name, last[1]);
openTransactionMap.put(id, t);
}
// TODO rollback all old, stored transactions (if there are any)
}
}
/**
* Get the list of currently open transactions.
*
* @return the list of transactions
*/
public synchronized List<Transaction> getOpenTransactions() {
ArrayList<Transaction> list = New.arrayList();
list.addAll(openTransactionMap.values());
return list;
}
/**
* Close the transaction store.
*/
public synchronized void close() {
settings.put("lastTransaction", "" + lastTransactionId);
// to avoid losing transaction ids (so just cosmetic)
settings.put(LAST_TRANSACTION_ID, "" + lastTransactionId);
store.commit();
}
/**
......@@ -87,15 +127,44 @@ public class TransactionStore {
* @return the transaction
*/
public synchronized Transaction begin() {
long baseVersion = store.getCurrentVersion();
store.incrementVersion();
long transactionId = lastTransactionId++;
if (lastTransactionId % 32 == 0) {
settings.put("lastTransaction", "" + lastTransactionId + 32);
if (lastTransactionId > lastTransactionIdStored) {
lastTransactionIdStored += 32;
settings.put(LAST_TRANSACTION_ID, "" + lastTransactionIdStored);
}
openTransactions.put(transactionId, baseVersion);
return new Transaction(this, transactionId);
int status = Transaction.STATUS_OPEN;
Object[] v = { status, null };
openTransactions.put(transactionId, v);
Transaction t = new Transaction(this, transactionId, status, null, 0);
openTransactionMap.put(transactionId, t);
return t;
}
/**
* Prepare a transaction.
*
* @param transactionId the transaction id
*/
void prepare(long transactionId) {
Object[] old = openTransactions.get(transactionId);
Object[] v = { Transaction.STATUS_PREPARED, old[1] };
openTransactions.put(transactionId, v);
store.commit();
}
/**
* Set the name of a transaction.
*
* @param transactionId the transaction id
* @param name the new name
*/
void setTransactionName(long transactionId, String name) {
Object[] old = openTransactions.get(transactionId);
Object[] v = { old[0], name };
openTransactions.put(transactionId, v);
store.commit();
}
/**
* Commit a transaction.
......@@ -104,27 +173,30 @@ public class TransactionStore {
* @param maxLogId the last log id
*/
void commit(long transactionId, long maxLogId) {
// TODO commit should be much faster
store.incrementVersion();
for (long logId = 0; logId < maxLogId; logId++) {
Object[] op = undoLog.get(new long[] {
transactionId, logId });
int mapId = ((Integer) op[1]).intValue();
Map<String, String> meta = store.getMetaMap();
String m = meta.get("map." + mapId);
String mapName = DataUtils.parseMap(m).get("name");
MVMap<Object, Object[]> map = store.openMap(mapName);
Object key = op[2];
Object[] value = map.get(key);
if (value == null) {
// already removed
} else if (value[2] == null) {
// remove the value
map.remove(key);
int opType = (Integer) op[0];
if (opType == Transaction.OP_REMOVE) {
int mapId = (Integer) op[1];
Map<String, String> meta = store.getMetaMap();
String m = meta.get("map." + mapId);
String mapName = DataUtils.parseMap(m).get("name");
MVMap<Object, Object[]> map = store.openMap(mapName);
Object key = op[2];
Object[] value = map.get(key);
// possibly the entry was added later on
// so we have to check
if (value[2] == null) {
// remove the value
map.remove(key);
}
}
undoLog.remove(logId);
}
openTransactions.remove(transactionId);
openTransactionMap.remove(transactionId);
store.commit();
}
......@@ -137,6 +209,7 @@ public class TransactionStore {
void rollback(long transactionId, long maxLogId) {
rollbackTo(transactionId, maxLogId, 0);
openTransactions.remove(transactionId);
openTransactionMap.remove(transactionId);
store.commit();
}
......@@ -187,6 +260,26 @@ public class TransactionStore {
* A transaction.
*/
public static class Transaction {
/**
* The status of an open transaction.
*/
public static final int STATUS_OPEN = 0;
/**
* The status of a prepared transaction.
*/
public static final int STATUS_PREPARED = 1;
/**
* The status of a closed transaction (committed or rolled back).
*/
public static final int STATUS_CLOSED = 2;
/**
* The operation type for changes in a map.
*/
static final int OP_REMOVE = 0, OP_ADD = 1, OP_SET = 2;
/**
* The transaction store.
......@@ -197,13 +290,57 @@ public class TransactionStore {
* The transaction id.
*/
final long transactionId;
private int status;
private String name;
private long logId;
private boolean closed;
Transaction(TransactionStore store, long transactionId) {
Transaction(TransactionStore store, long transactionId, int status, String name, long logId) {
this.store = store;
this.transactionId = transactionId;
this.status = status;
this.name = name;
this.logId = logId;
}
/**
* Get the transaction id.
*
* @return the transaction id
*/
public long getId() {
return transactionId;
}
/**
* Get the transaction status.
*
* @return the status
*/
public int getStatus() {
return status;
}
/**
* Set the name of the transaction.
*
* @param name the new name
*/
public void setName(String name) {
checkOpen();
store.setTransactionName(transactionId, name);
this.name = name;
}
/**
* Get the name of the transaction.
*
* @return name the name
*/
public String getName() {
return name;
}
/**
......@@ -212,6 +349,7 @@ public class TransactionStore {
* @return the savepoint id
*/
public long setSavepoint() {
checkOpen();
store.store.incrementVersion();
return logId;
}
......@@ -219,18 +357,18 @@ public class TransactionStore {
/**
* Add a log entry.
*
* @param baseVersion the old version
* @param opType the operation type
* @param mapId the map id
* @param key the key
*/
void log(long baseVersion, int mapId, Object key) {
void log(int opType, int mapId, Object key) {
long[] undoKey = { transactionId, logId++ };
Object[] log = new Object[] { baseVersion, mapId, key };
Object[] log = new Object[] { opType, mapId, key };
store.undoLog.put(undoKey, log);
}
/**
* Open a data map.
* Open a data map where reads are always up to date.
*
* @param <K> the key type
* @param <V> the value type
......@@ -238,40 +376,73 @@ public class TransactionStore {
* @return the transaction map
*/
public <K, V> TransactionMap<K, V> openMap(String name) {
return new TransactionMap<K, V>(this, name);
}
/**
* Commit the transaction. Afterwards, this transaction is closed.
*/
public void commit() {
closed = true;
store.commit(transactionId, logId);
checkOpen();
return new TransactionMap<K, V>(this, name, -1);
}
/**
* Roll the transaction back. Afterwards, this transaction is closed.
* Open a data map where reads are based on the specified version / savepoint.
*
* @param <K> the key type
* @param <V> the value type
* @param name the name of the map
* @param readVersion the version used for reading
* @return the transaction map
*/
public void rollback() {
closed = true;
store.rollback(transactionId, logId);
public <K, V> TransactionMap<K, V> openMap(String name, long readVersion) {
checkOpen();
// TODO read from a stable version of the data within
// one 'statement'
return new TransactionMap<K, V>(this, name, readVersion);
}
/**
* Roll back to the given savepoint.
*
* Roll back to the given savepoint. This is only allowed if the
* transaction is open.
*
* @param savepointId the savepoint id
*/
public void rollbackToSavepoint(long savepointId) {
checkOpen();
store.rollbackTo(transactionId, this.logId, savepointId);
this.logId = savepointId;
}
/**
* Prepare the transaction. Afterwards, the transaction can only be
* committed or rolled back.
*/
public void prepare() {
checkOpen();
store.prepare(transactionId);
status = STATUS_PREPARED;
}
/**
* Commit the transaction. Afterwards, this transaction is closed.
*/
public void commit() {
if (status != STATUS_CLOSED) {
store.commit(transactionId, logId);
status = STATUS_CLOSED;
}
}
/**
* Roll the transaction back. Afterwards, this transaction is closed.
*/
public void rollback() {
if (status != STATUS_CLOSED) {
store.rollback(transactionId, logId);
status = STATUS_CLOSED;
}
}
/**
* Check whether this transaction is still open.
*/
void checkOpen() {
if (closed) {
if (status != STATUS_OPEN) {
throw DataUtils.newIllegalStateException("Transaction is closed");
}
}
......@@ -288,29 +459,42 @@ public class TransactionStore {
private Transaction transaction;
private final int mapId;
/**
* The newest version of the data.
* Key: key.
* The map used for writing (the latest version).
* Key: key the key of the data.
* Value: { transactionId, oldVersion, value }
*/
private final MVMap<K, Object[]> map;
private final int mapId;
private final MVMap<K, Object[]> mapWrite;
/**
* The map used for reading (possibly an older version).
* Key: key the key of the data.
* Value: { transactionId, oldVersion, value }
*/
private final MVMap<K, Object[]> mapRead;
TransactionMap(Transaction transaction, String name) {
TransactionMap(Transaction transaction, String name, long readVersion) {
this.transaction = transaction;
map = transaction.store.store.openMap(name);
mapId = map.getId();
mapWrite = transaction.store.store.openMap(name);
mapId = mapWrite.getId();
if (readVersion >= 0) {
mapRead = mapWrite.openVersion(readVersion);
} else {
mapRead = mapWrite;
}
}
/**
* Get the size of the map as seen by this transaction.
*
* @return the size
*/
public long size() {
public long getSize() {
// TODO this method is very slow
long size = 0;
Cursor<K> cursor = map.keyIterator(null);
Cursor<K> cursor = mapRead.keyIterator(null);
while (cursor.hasNext()) {
K key = cursor.next();
if (get(key) != null) {
......@@ -325,20 +509,41 @@ public class TransactionStore {
}
/**
* Update the value for the given key. If the row is locked, this method
* Remove an entry.
* <p>
* If the row is locked, this method
* will retry until the row could be updated or until a lock timeout.
*
* @param key the key
* @throws IllegalStateException if a lock timeout occurs
*/
public V remove(K key) {
return set(key, null);
}
/**
* Update the value for the given key.
* <p>
* If the row is locked, this method
* will retry until the row could be updated or until a lock timeout.
*
* @param key the key
* @param value the new value (null to remove the row)
* @throws IllegalStateException if a lock timeout occurs
*/
public void put(K key, V value) {
public V put(K key, V value) {
DataUtils.checkArgument(value != null, "The value may not be null");
return set(key, value);
}
public V set(K key, V value) {
checkOpen();
long start = 0;
while (true) {
boolean ok = tryPut(key, value);
V old = get(key);
boolean ok = trySet(key, value);
if (ok) {
return;
return old;
}
// an uncommitted transaction:
// wait until it is committed, or until the lock timeout
......@@ -362,26 +567,60 @@ public class TransactionStore {
}
}
}
/**
* Try to remove the value for the given key.
* <p>
* This will fail if the row is locked by another transaction (that
* means, if another open transaction changed the row).
*
* @param key the key
* @return whether the entry could be removed
*/
public boolean tryRemove(K key) {
return trySet(key, null);
}
/**
* Try to update the value for the given key. This will fail if the row
* is not locked by another transaction (that means, if another open
* transaction added or updated the row).
* Try to update the value for the given key.
* <p>
* This will fail if the row is locked by another transaction (that
* means, if another open transaction changed the row).
*
* @param key the key
* @param value the new value
* @return whether the value could be updated
* @return whether the entry could be updated
*/
public boolean tryPut(K key, V value) {
Object[] current = map.get(key);
DataUtils.checkArgument(value != null, "The value may not be null");
return trySet(key, value);
}
private boolean trySet(K key, V value) {
Object[] current = mapWrite.get(key);
long oldVersion = transaction.store.store.getCurrentVersion() - 1;
int opType;
if (current == null || current[2] == null) {
if (value == null) {
// remove a removed value
opType = Transaction.OP_SET;
} else {
opType = Transaction.OP_ADD;
}
} else {
if (value == null) {
opType = Transaction.OP_REMOVE;
} else {
opType = Transaction.OP_SET;
}
}
Object[] newValue = { transaction.transactionId, oldVersion, value };
if (current == null) {
// a new value
newValue[1] = null;
Object[] old = map.putIfAbsent(key, newValue);
Object[] old = mapWrite.putIfAbsent(key, newValue);
if (old == null) {
transaction.log(oldVersion, mapId, key);
transaction.log(opType, mapId, key);
return true;
}
return false;
......@@ -389,13 +628,13 @@ public class TransactionStore {
long tx = ((Long) current[0]).longValue();
if (tx == transaction.transactionId) {
// added or updated by this transaction
if (map.replace(key, current, newValue)) {
if (mapWrite.replace(key, current, newValue)) {
if (current[1] == null) {
transaction.log(oldVersion, mapId, key);
transaction.log(opType, mapId, key);
} else {
long c = (Long) current[1];
if (c != oldVersion) {
transaction.log(oldVersion, mapId, key);
transaction.log(opType, mapId, key);
}
}
return true;
......@@ -405,12 +644,12 @@ public class TransactionStore {
return false;
}
// added or updated by another transaction
Long base = transaction.store.openTransactions.get(tx);
if (base == null) {
boolean open = transaction.store.openTransactions.containsKey(tx);
if (!open) {
// the transaction is committed:
// overwrite the value
if (map.replace(key, current, newValue)) {
transaction.log(oldVersion, mapId, key);
if (mapWrite.replace(key, current, newValue)) {
transaction.log(opType, mapId, key);
return true;
}
// somebody else was faster
......@@ -430,7 +669,7 @@ public class TransactionStore {
public
V get(K key) {
checkOpen();
MVMap<K, Object[]> m = map;
MVMap<K, Object[]> m = mapRead;
while (true) {
Object[] data = m.get(key);
long tx;
......@@ -444,8 +683,8 @@ public class TransactionStore {
return (V) data[2];
}
// added or updated by another transaction
Long base = transaction.store.openTransactions.get(tx);
if (base == null) {
boolean open = transaction.store.openTransactions.containsKey(tx);
if (!open) {
// it is committed
return (V) data[2];
}
......@@ -456,7 +695,7 @@ public class TransactionStore {
return null;
}
long oldVersion = (Long) data[1];
m = map.openVersion(oldVersion);
m = mapWrite.openVersion(oldVersion);
}
}
......
......@@ -12,12 +12,14 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.TransactionStore;
import org.h2.mvstore.TransactionStore.Transaction;
import org.h2.mvstore.TransactionStore.TransactionMap;
import org.h2.store.fs.FileUtils;
import org.h2.test.TestBase;
import org.h2.util.New;
......@@ -36,11 +38,142 @@ public class TestTransactionStore extends TestBase {
}
public void test() throws Exception {
// testMultiStatement();
testTwoPhaseCommit();
testSavepoint();
testConcurrentTransactionsReadCommitted();
testSingleConnection();
testCompareWithPostgreSQL();
}
/**
* Tests behavior when used for a sequence of SQL statements. Each statement
* uses a savepoint. Within a statement, a change by the statement itself is
* not seen; the change is only seen when the statement finished.
*/
// private void testMultiStatement() {
// MVStore s = MVStore.open(null);
// TransactionStore ts = new TransactionStore(s);
// Transaction tx;
// TransactionMap<String, String> m;
// long startUpdate;
//
// tx = ts.begin();
//
// // start of statement
// // insert into test(id, name) values(1, 'Hello'), (2, 'World')
// startUpdate = tx.setSavepoint();
// m = tx.openMap("test", startUpdate);
// TODO putIfAbsent
// m.put("1", "Hello");
// m.put("2", "World");
// // not seen yet
// assertNull(m.get("1"));
//
// // start of statement
// startUpdate = tx.setSavepoint();
// // update test set primaryKey = primaryKey + 1
// m = tx.openMap("test", startUpdate);
//
// for (Cursor)
//
// tx.commit();
//
//
//
// m.put("2", "World");
// m.put("1", "Hallo");
// m.remove("2");
// m.put("3", "!");
// long logId = tx.setSavepoint();
// m.put("1", "Hi");
// m.put("2", ".");
// m.remove("3");
// tx.rollbackToSavepoint(logId);
// assertEquals("Hallo", m.get("1"));
// assertNull(m.get("2"));
// assertEquals("!", m.get("3"));
// tx.rollback();
//
// tx = ts.begin();
// m = tx.openMap("test");
// assertNull(m.get("1"));
// assertNull(m.get("2"));
// assertNull(m.get("3"));
//
// ts.close();
// s.close();
// }
private void testTwoPhaseCommit() throws Exception {
String fileName = getBaseDir() + "/testTwoPhaseCommit.h3";
FileUtils.delete(fileName);
MVStore s;
TransactionStore ts;
Transaction tx;
Transaction txOld;
TransactionMap<String, String> m;
List<Transaction> list;
s = MVStore.open(fileName);
ts = new TransactionStore(s);
tx = ts.begin();
assertEquals(null, tx.getName());
tx.setName("first transaction");
assertEquals("first transaction", tx.getName());
assertEquals(0, tx.getId());
assertEquals(Transaction.STATUS_OPEN, tx.getStatus());
m = tx.openMap("test");
m.put("1", "Hello");
list = ts.getOpenTransactions();
assertEquals(1, list.size());
txOld = list.get(0);
assertTrue(tx == txOld);
s.commit();
ts.close();
s.close();
s = MVStore.open(fileName);
ts = new TransactionStore(s);
tx = ts.begin();
assertEquals(1, tx.getId());
m = tx.openMap("test");
assertEquals(null, m.get("1"));
list = ts.getOpenTransactions();
assertEquals(2, list.size());
txOld = list.get(0);
assertEquals(0, txOld.getId());
assertEquals(Transaction.STATUS_OPEN, txOld.getStatus());
assertEquals("first transaction", txOld.getName());
txOld.prepare();
assertEquals(Transaction.STATUS_PREPARED, txOld.getStatus());
txOld = list.get(1);
assertEquals(1, txOld.getId());
assertNull(txOld.getName());
assertEquals(Transaction.STATUS_OPEN, txOld.getStatus());
txOld.rollback();
s.commit();
s.close();
s = MVStore.open(fileName);
ts = new TransactionStore(s);
tx = ts.begin();
m = tx.openMap("test");
// TransactionStore was not closed, so we lost some ids
assertEquals(33, tx.getId());
list = ts.getOpenTransactions();
assertEquals(2, list.size());
txOld = list.get(0);
assertEquals(0, txOld.getId());
assertEquals(Transaction.STATUS_PREPARED, txOld.getStatus());
assertEquals("first transaction", txOld.getName());
txOld.commit();
assertEquals("Hello", m.get("1"));
s.close();
FileUtils.delete(fileName);
}
private void testSavepoint() throws Exception {
MVStore s = MVStore.open(null);
......@@ -53,12 +186,12 @@ public class TestTransactionStore extends TestBase {
m.put("1", "Hello");
m.put("2", "World");
m.put("1", "Hallo");
m.put("2", null);
m.remove("2");
m.put("3", "!");
long logId = tx.setSavepoint();
m.put("1", "Hi");
m.put("2", ".");
m.put("3", null);
m.remove("3");
tx.rollbackToSavepoint(logId);
assertEquals("Hallo", m.get("1"));
assertNull(m.get("2"));
......@@ -140,8 +273,8 @@ public class TestTransactionStore extends TestBase {
size++;
}
buff.append('\n');
if (size != map.size()) {
assertEquals(size, map.size());
if (size != map.getSize()) {
assertEquals(size, map.getSize());
}
}
int x = r.nextInt(rowCount);
......@@ -191,13 +324,13 @@ public class TestTransactionStore extends TestBase {
try {
int c = stat.executeUpdate("delete from test where id = " + x);
if (c == 1) {
map.put(x, null);
map.remove(x);
} else {
assertNull(map.get(x));
}
} catch (SQLException e) {
assertTrue(map.get(x) != null);
assertFalse(map.tryPut(x, null));
assertFalse(map.tryRemove(x));
// PostgreSQL needs to rollback
buff.append(" -> rollback");
stat.getConnection().rollback();
......@@ -245,7 +378,7 @@ public class TestTransactionStore extends TestBase {
m1 = tx1.openMap("test");
m1.put("1", "Hello");
m1.put("2", "World");
m1.put("3", null);
m1.remove("3");
tx1.commit();
// start new transaction to read old data
......@@ -256,7 +389,7 @@ public class TestTransactionStore extends TestBase {
tx1 = ts.begin();
m1 = tx1.openMap("test");
m1.put("1", "Hallo");
m1.put("2", null);
m1.remove("2");
m1.put("3", "!");
assertEquals("Hello", m2.get("1"));
......@@ -274,13 +407,13 @@ public class TestTransactionStore extends TestBase {
m1.put("2", "World");
assertNull(m2.get("2"));
assertFalse(m2.tryPut("2", null));
assertFalse(m2.tryRemove("2"));
assertFalse(m2.tryPut("2", "Welt"));
tx2 = ts.begin();
m2 = tx2.openMap("test");
assertNull(m2.get("2"));
m1.put("2", null);
m1.remove("2");
assertNull(m2.get("2"));
tx1.commit();
......@@ -337,7 +470,7 @@ public class TestTransactionStore extends TestBase {
tx = ts.begin();
m = tx.openMap("test");
m.put("1", "Hallo");
m.put("2", null);
m.remove("2");
m.put("3", "!");
assertEquals("Hallo", m.get("1"));
assertNull(m.get("2"));
......@@ -353,7 +486,7 @@ public class TestTransactionStore extends TestBase {
tx = ts.begin();
m = tx.openMap("test");
m.put("1", "Hallo");
m.put("2", null);
m.remove("2");
m.put("3", "!");
assertEquals("Hallo", m.get("1"));
assertNull(m.get("2"));
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论