Skip to content
项目
群组
代码片段
帮助
正在加载...
帮助
为 GitLab 提交贡献
登录/注册
切换导航
H
h2database
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
分枝图
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
计划
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
分枝图
统计图
创建新议题
作业
提交
议题看板
打开侧边栏
Administrator
h2database
Commits
5c4c6e46
提交
5c4c6e46
authored
12 年前
作者:
Thomas Mueller
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
MVStore: support concurrent transactions (MVCC style, should match the behavior of PostgreSQL)
上级
474b160e
隐藏空白字符变更
内嵌
并排
正在显示
2 个修改的文件
包含
376 行增加
和
80 行删除
+376
-80
MVMap.java
h2/src/main/org/h2/mvstore/MVMap.java
+1
-0
TestTransactionMap.java
h2/src/test/org/h2/test/store/TestTransactionMap.java
+375
-80
没有找到文件。
h2/src/main/org/h2/mvstore/MVMap.java
浏览文件 @
5c4c6e46
...
...
@@ -96,6 +96,7 @@ public class MVMap<K, V> extends AbstractMap<K, V>
*/
@SuppressWarnings
(
"unchecked"
)
public
V
put
(
K
key
,
V
value
)
{
DataUtils
.
checkArgument
(
value
!=
null
,
"The value may not be null"
);
beforeWrite
();
try
{
long
writeVersion
=
store
.
getCurrentVersion
();
...
...
This diff is collapsed.
Click to expand it.
h2/src/test/org/h2/test/store/TestTransactionMap.java
浏览文件 @
5c4c6e46
...
...
@@ -6,14 +6,21 @@
*/
package
org
.
h2
.
test
.
store
;
import
java.sql.Connection
;
import
java.sql.DriverManager
;
import
java.sql.ResultSet
;
import
java.sql.Statement
;
import
java.util.ArrayList
;
import
java.util.Iterator
;
import
java.util.Map
;
import
java.util.Random
;
import
org.h2.mvstore.DataUtils
;
import
org.h2.mvstore.MVMap
;
import
org.h2.mvstore.MVMapConcurrent
;
import
org.h2.mvstore.MVStore
;
import
org.h2.test.TestBase
;
import
org.h2.util.New
;
/**
* Test concurrent transactions.
...
...
@@ -29,20 +36,254 @@ public class TestTransactionMap extends TestBase {
TestBase
.
createCaller
().
init
().
test
();
}
public
void
test
()
{
public
void
test
()
throws
Exception
{
testConcurrentTransactions
();
testSingleConnection
();
// testCompareWithPostgreSQL();
}
public
void
testCompareWithPostgreSQL
()
throws
Exception
{
ArrayList
<
Statement
>
statements
=
New
.
arrayList
();
ArrayList
<
Transaction
>
transactions
=
New
.
arrayList
();
ArrayList
<
TransactionalMap
<
Integer
,
String
>>
maps
=
New
.
arrayList
();
int
connectionCount
=
4
,
opCount
=
1000
,
rowCount
=
10
;
try
{
Class
.
forName
(
"org.postgresql.Driver"
);
for
(
int
i
=
0
;
i
<
connectionCount
;
i
++)
{
Connection
conn
=
DriverManager
.
getConnection
(
"jdbc:postgresql:test"
,
"sa"
,
"sa"
);
statements
.
add
(
conn
.
createStatement
());
}
}
catch
(
Exception
e
)
{
// database not installed - ok
return
;
}
statements
.
get
(
0
).
execute
(
"drop table if exists test"
);
statements
.
get
(
0
).
execute
(
"create table test(id int primary key, name varchar(255))"
);
MVStore
s
=
MVStore
.
open
(
null
);
TransactionalStore
ts
=
new
TransactionalStore
(
s
);
for
(
int
i
=
0
;
i
<
connectionCount
;
i
++)
{
Statement
stat
=
statements
.
get
(
i
);
Connection
c
=
stat
.
getConnection
();
c
.
setTransactionIsolation
(
Connection
.
TRANSACTION_REPEATABLE_READ
);
c
.
setAutoCommit
(
false
);
Transaction
transaction
=
ts
.
begin
();
transactions
.
add
(
transaction
);
TransactionalMap
<
Integer
,
String
>
map
;
map
=
transaction
.
openMap
(
"test"
);
maps
.
add
(
map
);
}
StringBuilder
buff
=
new
StringBuilder
();
Random
r
=
new
Random
(
1
);
try
{
for
(
int
i
=
0
;
i
<
opCount
;
i
++)
{
int
connIndex
=
r
.
nextInt
(
connectionCount
);
Statement
stat
=
statements
.
get
(
connIndex
);
Transaction
transaction
=
transactions
.
get
(
connIndex
);
TransactionalMap
<
Integer
,
String
>
map
=
maps
.
get
(
connIndex
);
if
(
transaction
==
null
)
{
transaction
=
ts
.
begin
();
map
=
transaction
.
openMap
(
"test"
);
transactions
.
set
(
connIndex
,
transaction
);
maps
.
set
(
connIndex
,
map
);
// read all data, to get a snapshot
ResultSet
rs
=
stat
.
executeQuery
(
"select * from test order by id"
);
buff
.
append
(
"["
+
connIndex
+
"]="
);
while
(
rs
.
next
())
{
buff
.
append
(
' '
);
buff
.
append
(
rs
.
getInt
(
1
)).
append
(
':'
).
append
(
rs
.
getString
(
2
));
}
buff
.
append
(
'\n'
);
}
int
x
=
r
.
nextInt
(
rowCount
);
int
y
=
r
.
nextInt
(
rowCount
);
buff
.
append
(
"["
+
connIndex
+
"]: "
);
switch
(
r
.
nextInt
(
7
))
{
case
0
:
buff
.
append
(
"commit"
);
stat
.
getConnection
().
commit
();
transaction
.
commit
();
transactions
.
set
(
connIndex
,
null
);
break
;
case
1
:
buff
.
append
(
"rollback"
);
stat
.
getConnection
().
rollback
();
transaction
.
rollback
();
transactions
.
set
(
connIndex
,
null
);
break
;
case
2
:
// insert or update
if
(
i
==
98
)
{
int
test
;
System
.
out
.
println
(
map
.
get
(
x
));
}
String
old
=
map
.
get
(
x
);
if
(
old
==
null
)
{
buff
.
append
(
"insert "
+
x
+
"="
+
y
);
if
(
map
.
tryPut
(
x
,
""
+
y
))
{
stat
.
execute
(
"insert into test values("
+
x
+
", '"
+
y
+
"')"
);
}
else
{
// TODO how to check for locked rows in PostgreSQL?
}
}
else
{
buff
.
append
(
"update "
+
x
+
"="
+
y
+
" (old:"
+
old
+
")"
);
if
(
map
.
tryPut
(
x
,
""
+
y
))
{
int
c
=
stat
.
executeUpdate
(
"update test set name = '"
+
y
+
"' where id = "
+
x
);
if
(
c
==
0
)
{
int
test
;
System
.
out
.
println
(
map
.
get
(
x
));
}
assertEquals
(
1
,
c
);
}
else
{
// TODO how to check for locked rows in PostgreSQL?
}
}
break
;
case
3
:
buff
.
append
(
"delete "
+
x
);
stat
.
execute
(
"delete from test where id = "
+
x
);
map
.
put
(
x
,
null
);
break
;
case
4
:
case
5
:
case
6
:
ResultSet
rs
=
stat
.
executeQuery
(
"select * from test where id = "
+
x
);
String
expected
=
rs
.
next
()
?
rs
.
getString
(
2
)
:
null
;
buff
.
append
(
"select "
+
x
+
"="
+
expected
);
assertEquals
(
expected
,
map
.
get
(
x
));
break
;
}
buff
.
append
(
'\n'
);
}
}
catch
(
Exception
e
)
{
e
.
printStackTrace
();
fail
(
buff
.
toString
());
}
for
(
Statement
stat
:
statements
)
{
stat
.
getConnection
().
close
();
}
}
public
void
testConcurrentTransactions
()
{
MVStore
s
=
MVStore
.
open
(
null
);
TransactionalStore
ts
=
new
TransactionalStore
(
s
);
Transaction
tx
=
ts
.
begin
();
TransactionalMap
<
String
,
String
>
m
=
tx
.
openMap
(
"test"
);
Transaction
tx1
,
tx2
;
TransactionalMap
<
String
,
String
>
m1
,
m2
;
tx1
=
ts
.
begin
();
m1
=
tx1
.
openMap
(
"test"
);
m1
.
put
(
"1"
,
"Hi"
);
m1
.
put
(
"3"
,
"."
);
tx1
.
commit
();
tx1
=
ts
.
begin
();
m1
=
tx1
.
openMap
(
"test"
);
m1
.
put
(
"1"
,
"Hello"
);
m1
.
put
(
"2"
,
"World"
);
m1
.
put
(
"3"
,
null
);
tx1
.
commit
();
// start new transaction to read old data
tx2
=
ts
.
begin
();
m2
=
tx2
.
openMap
(
"test"
);
// start transaction tx1, update/delete/add
tx1
=
ts
.
begin
();
m1
=
tx1
.
openMap
(
"test"
);
m1
.
put
(
"1"
,
"Hallo"
);
m1
.
put
(
"2"
,
null
);
m1
.
put
(
"3"
,
"!"
);
tx1
.
commit
();
assertEquals
(
"Hello"
,
m2
.
get
(
"1"
));
assertEquals
(
"World"
,
m2
.
get
(
"2"
));
assertNull
(
m2
.
get
(
"3"
));
// even thought the row is locked,
// trying to remove it should work, as
// this key is unknown to this map
m2
.
put
(
"3"
,
null
);
// the row is locked, and trying to add a value
// should fail
assertFalse
(
m2
.
tryPut
(
"3"
,
"."
));
s
.
close
();
}
public
void
testSingleConnection
()
{
MVStore
s
=
MVStore
.
open
(
null
);
TransactionalStore
ts
=
new
TransactionalStore
(
s
);
Transaction
tx
;
TransactionalMap
<
String
,
String
>
m
;
// add, rollback
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
m
.
put
(
"1"
,
"Hello"
);
assertEquals
(
"Hello"
,
m
.
get
(
"1"
));
m
.
put
(
"2"
,
"World"
);
assertEquals
(
"World"
,
m
.
get
(
"2"
));
tx
.
rollback
();
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
assertNull
(
m
.
get
(
"1"
));
assertNull
(
m
.
get
(
"2"
));
// add, commit
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
m
.
put
(
"1"
,
"Hello"
);
m
.
put
(
"2"
,
"World"
);
assertEquals
(
"Hello"
,
m
.
get
(
"1"
));
assertEquals
(
"World"
,
m
.
get
(
"2"
));
tx
.
commit
();
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
assertEquals
(
"Hello"
,
m
.
get
(
"1"
));
assertEquals
(
"World"
,
m
.
get
(
"2"
));
// update+delete+insert, rollback
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
m
.
put
(
"1"
,
"Hallo"
);
m
.
put
(
"2"
,
null
);
m
.
put
(
"3"
,
"!"
);
assertEquals
(
"Hallo"
,
m
.
get
(
"1"
));
assertNull
(
m
.
get
(
"2"
));
assertEquals
(
"!"
,
m
.
get
(
"3"
));
tx
.
rollback
();
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
assertEquals
(
"Hello"
,
m
.
get
(
"1"
));
assertEquals
(
"World"
,
m
.
get
(
"2"
));
assertNull
(
m
.
get
(
"3"
));
// update+delete+insert, commit
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
m
.
put
(
"1"
,
"Hallo"
);
m
.
put
(
"2"
,
null
);
m
.
put
(
"3"
,
"!"
);
assertEquals
(
"Hallo"
,
m
.
get
(
"1"
));
assertNull
(
m
.
get
(
"2"
));
assertEquals
(
"!"
,
m
.
get
(
"3"
));
tx
.
commit
();
tx
=
ts
.
begin
();
m
=
tx
.
openMap
(
"test"
);
assertEquals
(
"Hallo"
,
m
.
get
(
"1"
));
assertNull
(
m
.
get
(
"2"
));
assertEquals
(
"!"
,
m
.
get
(
"3"
));
s
.
close
();
}
...
...
@@ -54,47 +295,67 @@ public class TestTransactionMap extends TestBase {
final
MVStore
store
;
/**
* The transaction settings. "lastTransaction" the last transaction id.
*/
final
MVMap
<
String
,
String
>
settings
;
// key: transactionId, value: baseVersion
final
MVMap
<
Long
,
Long
>
openTransactions
;
// key: [ transactionId, logId ], value: [ baseVersion, mapId, key ]
final
MVMap
<
long
[],
Object
[]>
undoLog
;
long
lockTimeout
=
1000
;
long
lastTransactionId
;
/**
* The lock timeout in milliseconds. 0 means timeout immediately.
*/
long
lockTimeout
;
TransactionalStore
(
MVStore
store
)
{
this
.
store
=
store
;
settings
=
store
.
openMap
(
"settings"
);
openTransactions
=
store
.
openMap
(
"openTransactions"
,
new
MVMapConcurrent
.
Builder
<
Long
,
Long
>());
undoLog
=
store
.
openMap
(
"undoLog"
,
new
MVMapConcurrent
.
Builder
<
long
[],
Object
[]>());
}
void
init
()
{
// TODO rollback all old, stored transactions (if there are any)
synchronized
void
init
()
{
String
s
=
settings
.
get
(
"lastTransaction"
);
if
(
s
!=
null
)
{
lastTransactionId
=
Long
.
parseLong
(
s
);
}
Long
t
=
openTransactions
.
lastKey
();
if
(
t
!=
null
)
{
if
(
t
.
longValue
()
>
lastTransactionId
)
{
throw
DataUtils
.
newIllegalStateException
(
"Last transaction not stored"
);
}
// TODO rollback all old, stored transactions (if there are any)
}
}
synchronized
void
close
()
{
settings
.
put
(
"lastTransaction"
,
""
+
lastTransactionId
);
}
Transaction
begin
()
{
long
baseVersion
;
long
transactionId
;
// repeat if necessary (when transactions are created concurrently)
// might use synchronization instead
while
(
true
)
{
baseVersion
=
store
.
incrementVersion
();
Long
t
=
openTransactions
.
lastKey
();
transactionId
=
t
==
null
?
0
:
t
.
longValue
()
+
1
;
t
=
openTransactions
.
putIfAbsent
(
transactionId
,
baseVersion
);
if
(
t
==
null
)
{
break
;
}
synchronized
Transaction
begin
()
{
long
baseVersion
=
store
.
getCurrentVersion
();
store
.
incrementVersion
();
long
transactionId
=
lastTransactionId
++;
if
(
lastTransactionId
%
32
==
0
)
{
settings
.
put
(
"lastTransaction"
,
""
+
lastTransactionId
+
32
);
}
openTransactions
.
put
(
transactionId
,
baseVersion
);
return
new
Transaction
(
this
,
transactionId
);
}
// TODO rollback in reverse order,
// to support delete & add of the same key
// with different baseVersions
// TODO return the undo operations instead
// TODO return the undo operations instead,
// so that index changed can be undone
public
void
endTransaction
(
boolean
success
,
long
transactionId
)
{
Iterator
<
long
[]>
it
=
undoLog
.
keyIterator
(
new
long
[]
{
transactionId
,
0
});
...
...
@@ -119,10 +380,8 @@ public class TestTransactionMap extends TestBase {
}
}
else
{
long
baseVersion
=
((
Long
)
op
[
0
]).
longValue
();
Object
[]
v
=
map
.
get
(
key
);
Object
value
=
v
[
1
];
Object
[]
old
;
if
(
baseVersion
>
=
map
.
getCreateVersion
())
{
if
(
baseVersion
<
=
map
.
getCreateVersion
())
{
// the map didn't exist yet
old
=
null
;
}
else
{
...
...
@@ -130,19 +389,12 @@ public class TestTransactionMap extends TestBase {
.
openVersion
(
baseVersion
-
1
);
old
=
mapOld
.
get
(
key
);
}
if
(
value
==
null
)
{
// this transaction deleted the value
map
.
put
(
key
,
old
);
if
(
old
==
null
)
{
// this transaction added the value
map
.
remove
(
key
);
}
else
{
if
(
old
==
null
)
{
// this transaction added the value
map
.
remove
(
key
);
}
else
{
// this transaction updated the value
map
.
put
(
key
,
old
);
}
// this transaction updated the value
map
.
put
(
key
,
old
);
}
}
undoLog
.
remove
(
k
);
...
...
@@ -161,6 +413,7 @@ public class TestTransactionMap extends TestBase {
final
long
transactionId
;
long
logId
;
long
baseVersion
;
private
boolean
closed
;
Transaction
(
TransactionalStore
store
,
long
transactionId
)
{
this
.
store
=
store
;
...
...
@@ -179,15 +432,23 @@ public class TestTransactionMap extends TestBase {
}
void
commit
()
{
closed
=
true
;
store
.
endTransaction
(
true
,
transactionId
);
}
void
rollback
()
{
closed
=
true
;
store
.
endTransaction
(
false
,
transactionId
);
}
}
void
checkOpen
()
{
if
(
closed
)
{
throw
DataUtils
.
newIllegalStateException
(
"Transaction is closed"
);
}
}
}
/**
* A map that supports transactions.
*
...
...
@@ -212,52 +473,31 @@ public class TestTransactionMap extends TestBase {
mapId
=
map
.
getId
();
oldMap
=
map
.
openVersion
(
transaction
.
baseVersion
);
}
private
void
checkOpen
()
{
transaction
.
checkOpen
();
}
void
put
(
K
key
,
V
value
)
{
checkOpen
();
long
start
=
0
;
while
(
true
)
{
Object
[]
current
=
map
.
get
(
key
);
Object
[]
newValue
=
{
transaction
.
transactionId
,
value
};
if
(
current
==
null
)
{
// a new value
Object
[]
old
=
map
.
putIfAbsent
(
key
,
newValue
);
if
(
old
==
null
)
{
transaction
.
log
(
transaction
.
baseVersion
,
mapId
,
key
);
return
;
}
// retry
continue
;
}
Object
[]
old
=
oldMap
.
get
(
key
);
long
tx
=
((
Long
)
old
[
0
]).
longValue
();
if
(
tx
==
transaction
.
transactionId
)
{
// update using the same transaction
if
(
map
.
replace
(
key
,
current
,
newValue
))
{
transaction
.
log
(
transaction
.
baseVersion
,
mapId
,
key
);
return
;
}
// retry
continue
;
}
Long
base
=
transaction
.
store
.
openTransactions
.
get
(
tx
);
if
(
base
==
null
)
{
// a committed transaction
// overwrite the value
if
(
map
.
replace
(
key
,
old
,
newValue
))
{
transaction
.
log
(
transaction
.
baseVersion
,
mapId
,
key
);
return
;
}
// retry
continue
;
boolean
ok
=
tryPut
(
key
,
value
);
if
(
ok
)
{
return
;
}
// an uncommitted transaction:
// wait until it is committed, or until the lock timeout
long
timeout
=
transaction
.
store
.
lockTimeout
;
if
(
timeout
==
0
)
{
throw
DataUtils
.
newIllegalStateException
(
"Lock timeout"
);
}
if
(
start
==
0
)
{
start
=
System
.
currentTimeMillis
();
}
else
{
long
t
=
System
.
currentTimeMillis
()
-
start
;
if
(
t
>
t
ransaction
.
store
.
lockT
imeout
)
{
throw
new
IllegalStateException
(
"Lock timeout"
);
if
(
t
>
timeout
)
{
throw
DataUtils
.
new
IllegalStateException
(
"Lock timeout"
);
}
try
{
Thread
.
sleep
(
1
);
...
...
@@ -268,16 +508,69 @@ public class TestTransactionMap extends TestBase {
}
}
public
boolean
tryPut
(
K
key
,
V
value
)
{
if
(
tryReplace
(
key
,
value
))
{
return
true
;
}
if
(
value
==
null
)
{
// trying to remove a row that is invisible to this
// transaction is a no-op
return
true
;
}
return
false
;
}
public
boolean
tryReplace
(
K
key
,
V
value
)
{
Object
[]
current
=
map
.
get
(
key
);
Object
[]
newValue
=
{
transaction
.
transactionId
,
value
};
if
(
current
==
null
)
{
// a new value
Object
[]
old
=
map
.
putIfAbsent
(
key
,
newValue
);
if
(
old
==
null
)
{
transaction
.
log
(
transaction
.
baseVersion
,
mapId
,
key
);
return
true
;
}
return
false
;
}
Object
[]
old
=
oldMap
.
get
(
key
);
if
(
old
==
null
)
{
// added by another transaction
return
false
;
}
long
tx
=
((
Long
)
old
[
0
]).
longValue
();
if
(
tx
==
transaction
.
transactionId
)
{
// update using the same transaction
if
(
map
.
replace
(
key
,
current
,
newValue
))
{
transaction
.
log
(
transaction
.
baseVersion
,
mapId
,
key
);
return
true
;
}
return
false
;
}
Long
base
=
transaction
.
store
.
openTransactions
.
get
(
tx
);
if
(
base
==
null
)
{
// from a transaction that was committed
// when this transaction began:
// overwrite the value
if
(
map
.
replace
(
key
,
old
,
newValue
))
{
transaction
.
log
(
transaction
.
baseVersion
,
mapId
,
key
);
return
true
;
}
}
return
false
;
}
@SuppressWarnings
(
"unchecked"
)
V
get
(
K
key
)
{
checkOpen
();
Object
[]
old
=
oldMap
.
get
(
key
);
Object
[]
current
=
map
.
get
(
key
);
long
tx
;
if
(
old
==
null
)
{
if
(
current
==
null
)
{
// didn't exist before and doesn't exist now
return
null
;
}
long
tx
=
((
Long
)
current
[
0
]).
longValue
();
tx
=
((
Long
)
current
[
0
]).
longValue
();
if
(
tx
==
transaction
.
transactionId
)
{
// added by this transaction
return
(
V
)
current
[
1
];
...
...
@@ -287,21 +580,22 @@ public class TestTransactionMap extends TestBase {
}
else
if
(
current
==
null
)
{
// deleted by a committed transaction
// which means not by the current transaction
return
(
V
)
old
[
1
];
}
long
tx
=
((
Long
)
current
[
0
]).
longValue
();
if
(
tx
==
transaction
.
transactionId
)
{
// updated by this transaction
return
(
V
)
current
[
1
];
tx
=
((
Long
)
old
[
0
]).
longValue
();
}
else
{
tx
=
((
Long
)
current
[
0
]).
longValue
();
if
(
tx
==
transaction
.
transactionId
)
{
// updated by this transaction
return
(
V
)
current
[
1
];
}
}
// updated by another transaction
Long
base
=
transaction
.
store
.
openTransactions
.
get
(
t
ransaction
);
Long
base
=
transaction
.
store
.
openTransactions
.
get
(
t
x
);
if
(
base
==
null
)
{
// it was committed
return
(
V
)
old
[
1
];
}
// get the value before the uncommitted transaction
MVMap
<
K
,
Object
[]>
olderMap
=
oldM
ap
.
openVersion
(
base
.
longValue
());
MVMap
<
K
,
Object
[]>
olderMap
=
m
ap
.
openVersion
(
base
.
longValue
());
old
=
olderMap
.
get
(
key
);
if
(
old
==
null
)
{
// there was none
...
...
@@ -312,4 +606,5 @@ public class TestTransactionMap extends TestBase {
}
}
}
This diff is collapsed.
Click to expand it.
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论