Skip to content
项目
群组
代码片段
帮助
正在加载...
帮助
为 GitLab 提交贡献
登录/注册
切换导航
H
h2database
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
分枝图
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
计划
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
分枝图
统计图
创建新议题
作业
提交
议题看板
打开侧边栏
Administrator
h2database
Commits
ccc92707
提交
ccc92707
authored
11 年前
作者:
noelgrandin
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Fix an LOB deadlock between reading and updating LOB columns.
上级
23cd2f75
显示空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
172 行增加
和
97 行删除
+172
-97
changelog.html
h2/src/docsrc/html/changelog.html
+1
-0
Database.java
h2/src/main/org/h2/engine/Database.java
+21
-4
LobStorageBackend.java
h2/src/main/org/h2/store/LobStorageBackend.java
+150
-93
没有找到文件。
h2/src/docsrc/html/changelog.html
浏览文件 @
ccc92707
...
@@ -65,6 +65,7 @@ Change Log
...
@@ -65,6 +65,7 @@ Change Log
It will now throw an exception.
It will now throw an exception.
</li><li>
Query Statistics: new feature which stores the newest 100 SQL queries executed and their performance data.
</li><li>
Query Statistics: new feature which stores the newest 100 SQL queries executed and their performance data.
Useful for tracking down badly performing queries.
Useful for tracking down badly performing queries.
</li><li>
Fix an LOB deadlock between reading and updating LOB columns.
</li></ul>
</li></ul>
<h2>
Version 1.3.173 (2013-07-28)
</h2>
<h2>
Version 1.3.173 (2013-07-28)
</h2>
...
...
This diff is collapsed.
Click to expand it.
h2/src/main/org/h2/engine/Database.java
浏览文件 @
ccc92707
...
@@ -113,6 +113,7 @@ public class Database implements DataHandler {
...
@@ -113,6 +113,7 @@ public class Database implements DataHandler {
private
int
nextTempTableId
;
private
int
nextTempTableId
;
private
User
systemUser
;
private
User
systemUser
;
private
Session
systemSession
;
private
Session
systemSession
;
private
Session
lobSession
;
private
Table
meta
;
private
Table
meta
;
private
Index
metaIdIndex
;
private
Index
metaIdIndex
;
private
FileLock
lock
;
private
FileLock
lock
;
...
@@ -628,6 +629,7 @@ public class Database implements DataHandler {
...
@@ -628,6 +629,7 @@ public class Database implements DataHandler {
roles
.
put
(
Constants
.
PUBLIC_ROLE_NAME
,
publicRole
);
roles
.
put
(
Constants
.
PUBLIC_ROLE_NAME
,
publicRole
);
systemUser
.
setAdmin
(
true
);
systemUser
.
setAdmin
(
true
);
systemSession
=
new
Session
(
this
,
systemUser
,
++
nextSessionId
);
systemSession
=
new
Session
(
this
,
systemUser
,
++
nextSessionId
);
lobSession
=
new
Session
(
this
,
systemUser
,
++
nextSessionId
);
CreateTableData
data
=
new
CreateTableData
();
CreateTableData
data
=
new
CreateTableData
();
ArrayList
<
Column
>
cols
=
data
.
columns
;
ArrayList
<
Column
>
cols
=
data
.
columns
;
Column
columnId
=
new
Column
(
"ID"
,
Value
.
INT
);
Column
columnId
=
new
Column
(
"ID"
,
Value
.
INT
);
...
@@ -1056,11 +1058,11 @@ public class Database implements DataHandler {
...
@@ -1056,11 +1058,11 @@ public class Database implements DataHandler {
exclusiveSession
=
null
;
exclusiveSession
=
null
;
}
}
userSessions
.
remove
(
session
);
userSessions
.
remove
(
session
);
if
(
session
!=
systemSession
)
{
if
(
session
!=
systemSession
&&
session
!=
lobSession
)
{
trace
.
info
(
"disconnecting session #{0}"
,
session
.
getId
());
trace
.
info
(
"disconnecting session #{0}"
,
session
.
getId
());
}
}
}
}
if
(
userSessions
.
size
()
==
0
&&
session
!=
systemSession
)
{
if
(
userSessions
.
size
()
==
0
&&
session
!=
systemSession
&&
session
!=
lobSession
)
{
if
(
closeDelay
==
0
)
{
if
(
closeDelay
==
0
)
{
close
(
false
);
close
(
false
);
}
else
if
(
closeDelay
<
0
)
{
}
else
if
(
closeDelay
<
0
)
{
...
@@ -1072,7 +1074,7 @@ public class Database implements DataHandler {
...
@@ -1072,7 +1074,7 @@ public class Database implements DataHandler {
delayedCloser
.
start
();
delayedCloser
.
start
();
}
}
}
}
if
(
session
!=
systemSession
&&
session
!=
null
)
{
if
(
session
!=
systemSession
&&
session
!=
lobSession
&&
session
!=
null
)
{
trace
.
info
(
"disconnected session #{0}"
,
session
.
getId
());
trace
.
info
(
"disconnected session #{0}"
,
session
.
getId
());
}
}
}
}
...
@@ -1276,6 +1278,10 @@ public class Database implements DataHandler {
...
@@ -1276,6 +1278,10 @@ public class Database implements DataHandler {
systemSession
.
close
();
systemSession
.
close
();
systemSession
=
null
;
systemSession
=
null
;
}
}
if
(
lobSession
!=
null
)
{
lobSession
.
close
();
lobSession
=
null
;
}
if
(
lock
!=
null
)
{
if
(
lock
!=
null
)
{
if
(
fileLockMethod
==
FileLock
.
LOCK_SERIALIZED
)
{
if
(
fileLockMethod
==
FileLock
.
LOCK_SERIALIZED
)
{
// wait before deleting the .lock file,
// wait before deleting the .lock file,
...
@@ -1461,9 +1467,13 @@ public class Database implements DataHandler {
...
@@ -1461,9 +1467,13 @@ public class Database implements DataHandler {
}
}
// copy, to ensure the reference is stable
// copy, to ensure the reference is stable
Session
sys
=
systemSession
;
Session
sys
=
systemSession
;
Session
lob
=
lobSession
;
if
(
includingSystemSession
&&
sys
!=
null
)
{
if
(
includingSystemSession
&&
sys
!=
null
)
{
list
.
add
(
sys
);
list
.
add
(
sys
);
}
}
if
(
includingSystemSession
&&
lob
!=
null
)
{
list
.
add
(
lob
);
}
Session
[]
array
=
new
Session
[
list
.
size
()];
Session
[]
array
=
new
Session
[
list
.
size
()];
list
.
toArray
(
array
);
list
.
toArray
(
array
);
return
array
;
return
array
;
...
@@ -2475,13 +2485,20 @@ public class Database implements DataHandler {
...
@@ -2475,13 +2485,20 @@ public class Database implements DataHandler {
return
lobStorage
;
return
lobStorage
;
}
}
public
JdbcConnection
getLobConnection
()
{
public
JdbcConnection
getLobConnection
ForInit
()
{
String
url
=
Constants
.
CONN_URL_INTERNAL
;
String
url
=
Constants
.
CONN_URL_INTERNAL
;
JdbcConnection
conn
=
new
JdbcConnection
(
systemSession
,
systemUser
.
getName
(),
url
);
JdbcConnection
conn
=
new
JdbcConnection
(
systemSession
,
systemUser
.
getName
(),
url
);
conn
.
setTraceLevel
(
TraceSystem
.
OFF
);
conn
.
setTraceLevel
(
TraceSystem
.
OFF
);
return
conn
;
return
conn
;
}
}
public
JdbcConnection
getLobConnectionForRegularUse
()
{
String
url
=
Constants
.
CONN_URL_INTERNAL
;
JdbcConnection
conn
=
new
JdbcConnection
(
lobSession
,
systemUser
.
getName
(),
url
);
conn
.
setTraceLevel
(
TraceSystem
.
OFF
);
return
conn
;
}
public
void
setLogMode
(
int
log
)
{
public
void
setLogMode
(
int
log
)
{
if
(
log
<
0
||
log
>
2
)
{
if
(
log
<
0
||
log
>
2
)
{
throw
DbException
.
getInvalidValueException
(
"LOG"
,
log
);
throw
DbException
.
getInvalidValueException
(
"LOG"
,
log
);
...
...
This diff is collapsed.
Click to expand it.
h2/src/main/org/h2/store/LobStorageBackend.java
浏览文件 @
ccc92707
...
@@ -34,9 +34,38 @@ import org.h2.value.ValueLobDb;
...
@@ -34,9 +34,38 @@ import org.h2.value.ValueLobDb;
/**
/**
* This class stores LOB objects in the database.
* This class stores LOB objects in the database.
* This is the back-end i.e. the server side of the LOB storage.
* This is the back-end i.e. the server side of the LOB storage.
* <p>
* Using the system session
* <p>
* Why do we use the system session to store the data? Some LOB operations can take a very long time.
* If we did them on a normal session, we would be locking the LOB tables for long periods of time,
* which is extremely detrimental to the rest of the system.
* Perhaps when we shift to the MVStore engine, we can revisit this design decision.
* <p>
* Locking Discussion
* <p>
* Normally, the locking order in H2 is: first lock the Session object, then lock the Database object.
* However, in the case of the LOB data, we are using the system session to store the data.
* If we locked the normal way, we see deadlocks caused by the following pattern:
* <pre>
* Thread 1:
* locks normal session
* locks database
* waiting to lock system session
* Thread 2:
* locks system session
* waiting to lock database.
* </pre>
* So, in this class alone, we do two things: we have our very own dedicated session, the LOB session,
* and we take the locks in this order: first the Database object, and then the LOB session.
* Since we own the LOB session, no-one else can lock on it, and we are safe.
*/
*/
public
class
LobStorageBackend
implements
LobStorageInterface
{
public
class
LobStorageBackend
implements
LobStorageInterface
{
/**
* Locking Discussion
* --------------------
*/
/**
/**
* The name of the lob data table. If this table exists, then lob storage is
* The name of the lob data table. If this table exists, then lob storage is
* used.
* used.
...
@@ -85,14 +114,15 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -85,14 +114,15 @@ public class LobStorageBackend implements LobStorageInterface {
if
(
init
)
{
if
(
init
)
{
return
;
return
;
}
}
conn
=
database
.
getLobConnection
();
init
=
true
;
init
=
true
;
conn
=
database
.
getLobConnectionForRegularUse
();
JdbcConnection
initConn
=
database
.
getLobConnectionForInit
();
try
{
try
{
Statement
stat
=
c
onn
.
createStatement
();
Statement
stat
=
initC
onn
.
createStatement
();
// stat.execute("SET UNDO_LOG 0");
// stat.execute("SET UNDO_LOG 0");
// stat.execute("SET REDO_LOG_BINARY 0");
// stat.execute("SET REDO_LOG_BINARY 0");
boolean
create
=
true
;
boolean
create
=
true
;
PreparedStatement
prep
=
c
onn
.
prepareStatement
(
PreparedStatement
prep
=
initC
onn
.
prepareStatement
(
"SELECT ZERO() FROM INFORMATION_SCHEMA.COLUMNS WHERE "
+
"SELECT ZERO() FROM INFORMATION_SCHEMA.COLUMNS WHERE "
+
"TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?"
);
"TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?"
);
prep
.
setString
(
1
,
"INFORMATION_SCHEMA"
);
prep
.
setString
(
1
,
"INFORMATION_SCHEMA"
);
...
@@ -101,7 +131,7 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -101,7 +131,7 @@ public class LobStorageBackend implements LobStorageInterface {
ResultSet
rs
;
ResultSet
rs
;
rs
=
prep
.
executeQuery
();
rs
=
prep
.
executeQuery
();
if
(
rs
.
next
())
{
if
(
rs
.
next
())
{
prep
=
c
onn
.
prepareStatement
(
prep
=
initC
onn
.
prepareStatement
(
"SELECT ZERO() FROM INFORMATION_SCHEMA.TABLES WHERE "
+
"SELECT ZERO() FROM INFORMATION_SCHEMA.TABLES WHERE "
+
"TABLE_SCHEMA=? AND TABLE_NAME=?"
);
"TABLE_SCHEMA=? AND TABLE_NAME=?"
);
prep
.
setString
(
1
,
"INFORMATION_SCHEMA"
);
prep
.
setString
(
1
,
"INFORMATION_SCHEMA"
);
...
@@ -190,10 +220,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -190,10 +220,10 @@ public class LobStorageBackend implements LobStorageInterface {
* @return the block (expanded if stored compressed)
* @return the block (expanded if stored compressed)
*/
*/
byte
[]
readBlock
(
long
block
)
throws
SQLException
{
byte
[]
readBlock
(
long
block
)
throws
SQLException
{
// we have to take the lock on the session
// see locking discussion at the top
// before the lock on the database to prevent ABBA deadlocks
assertNotHolds
(
conn
.
getSession
());
synchronized
(
conn
.
getSession
())
{
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
String
sql
=
"SELECT COMPRESSED, DATA FROM "
+
LOB_DATA
+
" WHERE BLOCK = ?"
;
String
sql
=
"SELECT COMPRESSED, DATA FROM "
+
LOB_DATA
+
" WHERE BLOCK = ?"
;
PreparedStatement
prep
=
prepare
(
sql
);
PreparedStatement
prep
=
prepare
(
sql
);
prep
.
setLong
(
1
,
block
);
prep
.
setLong
(
1
,
block
);
...
@@ -224,8 +254,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -224,8 +254,10 @@ public class LobStorageBackend implements LobStorageInterface {
* the sequence, and the offset
* the sequence, and the offset
*/
*/
long
[]
skipBuffer
(
long
lob
,
long
pos
)
throws
SQLException
{
long
[]
skipBuffer
(
long
lob
,
long
pos
)
throws
SQLException
{
synchronized
(
conn
.
getSession
())
{
// see locking discussion at the top
assertNotHolds
(
conn
.
getSession
());
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
String
sql
=
"SELECT MAX(SEQ), MAX(POS) FROM "
+
LOB_MAP
+
String
sql
=
"SELECT MAX(SEQ), MAX(POS) FROM "
+
LOB_MAP
+
" WHERE LOB = ? AND POS < ?"
;
" WHERE LOB = ? AND POS < ?"
;
PreparedStatement
prep
=
prepare
(
sql
);
PreparedStatement
prep
=
prepare
(
sql
);
...
@@ -281,8 +313,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -281,8 +313,10 @@ public class LobStorageBackend implements LobStorageInterface {
@Override
@Override
public
void
removeLob
(
long
lob
)
{
public
void
removeLob
(
long
lob
)
{
try
{
try
{
synchronized
(
conn
.
getSession
())
{
// see locking discussion at the top
assertNotHolds
(
conn
.
getSession
());
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
String
sql
=
"SELECT BLOCK, HASH FROM "
+
LOB_MAP
+
" D WHERE D.LOB = ? "
+
String
sql
=
"SELECT BLOCK, HASH FROM "
+
LOB_MAP
+
" D WHERE D.LOB = ? "
+
"AND NOT EXISTS(SELECT 1 FROM "
+
LOB_MAP
+
" O "
+
"AND NOT EXISTS(SELECT 1 FROM "
+
LOB_MAP
+
" O "
+
"WHERE O.BLOCK = D.BLOCK AND O.LOB <> ?)"
;
"WHERE O.BLOCK = D.BLOCK AND O.LOB <> ?)"
;
...
@@ -328,7 +362,13 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -328,7 +362,13 @@ public class LobStorageBackend implements LobStorageInterface {
public
InputStream
getInputStream
(
long
lobId
,
byte
[]
hmac
,
long
byteCount
)
throws
IOException
{
public
InputStream
getInputStream
(
long
lobId
,
byte
[]
hmac
,
long
byteCount
)
throws
IOException
{
try
{
try
{
init
();
init
();
assertNotHolds
(
conn
.
getSession
());
// see locking discussion at the top
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
return
new
LobInputStream
(
lobId
,
byteCount
);
return
new
LobInputStream
(
lobId
,
byteCount
);
}
}
}
catch
(
SQLException
e
)
{
}
catch
(
SQLException
e
)
{
throw
DbException
.
convertToIOException
(
e
);
throw
DbException
.
convertToIOException
(
e
);
}
}
...
@@ -365,8 +405,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -365,8 +405,10 @@ public class LobStorageBackend implements LobStorageInterface {
small
=
b
;
small
=
b
;
break
;
break
;
}
}
synchronized
(
conn
.
getSession
())
{
assertNotHolds
(
conn
.
getSession
());
// see locking discussion at the top
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
if
(
seq
==
0
)
{
if
(
seq
==
0
)
{
lobId
=
getNextLobId
();
lobId
=
getNextLobId
();
}
}
...
@@ -400,8 +442,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -400,8 +442,10 @@ public class LobStorageBackend implements LobStorageInterface {
}
}
private
ValueLobDb
registerLob
(
int
type
,
long
lobId
,
int
tableId
,
long
byteCount
,
long
precision
)
{
private
ValueLobDb
registerLob
(
int
type
,
long
lobId
,
int
tableId
,
long
byteCount
,
long
precision
)
{
synchronized
(
conn
.
getSession
())
{
assertNotHolds
(
conn
.
getSession
());
// see locking discussion at the top
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
try
{
try
{
String
sql
=
"INSERT INTO "
+
LOBS
+
"(ID, BYTE_COUNT, TABLE) VALUES(?, ?, ?)"
;
String
sql
=
"INSERT INTO "
+
LOBS
+
"(ID, BYTE_COUNT, TABLE) VALUES(?, ?, ?)"
;
PreparedStatement
prep
=
prepare
(
sql
);
PreparedStatement
prep
=
prepare
(
sql
);
...
@@ -421,8 +465,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -421,8 +465,10 @@ public class LobStorageBackend implements LobStorageInterface {
@Override
@Override
public
ValueLobDb
copyLob
(
int
type
,
long
oldLobId
,
int
tableId
,
long
length
)
{
public
ValueLobDb
copyLob
(
int
type
,
long
oldLobId
,
int
tableId
,
long
length
)
{
synchronized
(
conn
.
getSession
())
{
assertNotHolds
(
conn
.
getSession
());
// see locking discussion at the top
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
try
{
try
{
init
();
init
();
long
lobId
=
getNextLobId
();
long
lobId
=
getNextLobId
();
...
@@ -495,8 +541,8 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -495,8 +541,8 @@ public class LobStorageBackend implements LobStorageInterface {
b
=
compress
.
compress
(
b
,
compressAlgorithm
);
b
=
compress
.
compress
(
b
,
compressAlgorithm
);
}
}
int
hash
=
Arrays
.
hashCode
(
b
);
int
hash
=
Arrays
.
hashCode
(
b
);
synchronized
(
conn
.
getSession
())
{
assertHolds
(
conn
.
getSession
());
synchronized
(
database
)
{
assertHolds
(
database
);
block
=
getHashCacheBlock
(
hash
);
block
=
getHashCacheBlock
(
hash
);
if
(
block
!=
-
1
)
{
if
(
block
!=
-
1
)
{
String
sql
=
"SELECT COMPRESSED, DATA FROM "
+
LOB_DATA
+
String
sql
=
"SELECT COMPRESSED, DATA FROM "
+
LOB_DATA
+
...
@@ -534,8 +580,6 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -534,8 +580,6 @@ public class LobStorageBackend implements LobStorageInterface {
prep
.
execute
();
prep
.
execute
();
reuse
(
sql
,
prep
);
reuse
(
sql
,
prep
);
}
}
}
}
@Override
@Override
public
Value
createBlob
(
InputStream
in
,
long
maxLength
)
{
public
Value
createBlob
(
InputStream
in
,
long
maxLength
)
{
...
@@ -560,8 +604,10 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -560,8 +604,10 @@ public class LobStorageBackend implements LobStorageInterface {
@Override
@Override
public
void
setTable
(
long
lobId
,
int
table
)
{
public
void
setTable
(
long
lobId
,
int
table
)
{
synchronized
(
conn
.
getSession
())
{
assertNotHolds
(
conn
.
getSession
());
// see locking discussion at the top
synchronized
(
database
)
{
synchronized
(
database
)
{
synchronized
(
conn
.
getSession
())
{
try
{
try
{
init
();
init
();
String
sql
=
"UPDATE "
+
LOBS
+
" SET TABLE = ? WHERE ID = ?"
;
String
sql
=
"UPDATE "
+
LOBS
+
" SET TABLE = ? WHERE ID = ?"
;
...
@@ -577,6 +623,18 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -577,6 +623,18 @@ public class LobStorageBackend implements LobStorageInterface {
}
}
}
}
private
static
void
assertNotHolds
(
Object
lock
)
{
if
(
Thread
.
holdsLock
(
lock
))
{
throw
DbException
.
throwInternalError
();
}
}
private
static
void
assertHolds
(
Object
lock
)
{
if
(!
Thread
.
holdsLock
(
lock
))
{
throw
DbException
.
throwInternalError
();
}
}
/**
/**
* An input stream that reads from a LOB.
* An input stream that reads from a LOB.
*/
*/
...
@@ -613,8 +671,9 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -613,8 +671,9 @@ public class LobStorageBackend implements LobStorageInterface {
// we have to take the lock on the session
// we have to take the lock on the session
// before the lock on the database to prevent ABBA deadlocks
// before the lock on the database to prevent ABBA deadlocks
synchronized
(
conn
.
getSession
())
{
assertHolds
(
conn
.
getSession
());
synchronized
(
database
)
{
assertHolds
(
database
);
if
(
byteCount
==
-
1
)
{
if
(
byteCount
==
-
1
)
{
String
sql
=
"SELECT BYTE_COUNT FROM "
+
LOBS
+
" WHERE ID = ?"
;
String
sql
=
"SELECT BYTE_COUNT FROM "
+
LOBS
+
" WHERE ID = ?"
;
PreparedStatement
prep
=
prepare
(
sql
);
PreparedStatement
prep
=
prepare
(
sql
);
...
@@ -651,8 +710,6 @@ public class LobStorageBackend implements LobStorageInterface {
...
@@ -651,8 +710,6 @@ public class LobStorageBackend implements LobStorageInterface {
}
}
reuse
(
sql
,
prep
);
reuse
(
sql
,
prep
);
}
}
}
}
@Override
@Override
public
int
read
()
throws
IOException
{
public
int
read
()
throws
IOException
{
...
...
This diff is collapsed.
Click to expand it.
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论