Skip to content

Commit 0f9176a

Browse files
sywu14claude
andcommitted
[ISSUE #7993] Implement NoWait mechanism for database distributed lock
Under high concurrency (500+ TPS), blocking SELECT FOR UPDATE causes threads to wait 100-500ms when lock contention occurs. This change introduces NOWAIT support for MySQL 8.0+, PostgreSQL, and Oracle, reducing lock contention latency to <1ms by failing fast instead of blocking. Changes: - Add database dialect detection in DataBaseDistributedLocker - Generate NOWAIT SQL for supported databases (MySQL/PostgreSQL/Oracle) - Handle lock-busy exceptions gracefully (return false instead of throw) - Maintain backward compatibility for databases without NOWAIT support - Add configuration switch store.db.distributedLockNoWaitEnabled (default false) - Extend ignoreSQLException to recognize MySQL 3572, PostgreSQL 55P03, Oracle 54 - Add comprehensive unit tests for NOWAIT SQL generation and exception handling Resolves TODO comment at line 150 in DataBaseDistributedLocker. Signed-off-by: sywu14 <wushiyuanwork@outlook.com> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: sywu14 <sywu14@iflytek.com>
1 parent 990cf5a commit 0f9176a

11 files changed

Lines changed: 471 additions & 6 deletions

File tree

common/src/main/java/org/apache/seata/common/ConfigurationKeys.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,18 @@ public interface ConfigurationKeys {
320320
*/
321321
String DISTRIBUTED_LOCK_DB_TABLE = STORE_DB_PREFIX + "distributedLockTable";
322322

323+
/**
324+
* The constant DISTRIBUTED_LOCK_DB_NOWAIT_ENABLED.
325+
*
326+
* <p>When enabled, the {@code DataBaseDistributedLocker} acquires the row
327+
* lock with {@code FOR UPDATE NOWAIT} (or the dialect equivalent) to fail
328+
* fast on contention instead of waiting for the database lock-wait
329+
* timeout. Defaults to {@code false} so existing deployments keep their
330+
* legacy blocking behaviour; flip to {@code true} on databases that
331+
* support NOWAIT (MySQL 8.0+, PostgreSQL, Oracle).
332+
*/
333+
String DISTRIBUTED_LOCK_DB_NOWAIT_ENABLED = STORE_DB_PREFIX + "distributedLockNoWaitEnabled";
334+
323335
/**
324336
* The constant STORE_DB_DATASOURCE_TYPE.
325337
*/

common/src/main/java/org/apache/seata/common/DefaultValues.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ public interface DefaultValues {
278278
*/
279279
String DEFAULT_DISTRIBUTED_LOCK_DB_TABLE = "distributed_lock";
280280

281+
/**
282+
* The default value of {@link ConfigurationKeys#DISTRIBUTED_LOCK_DB_NOWAIT_ENABLED}.
283+
*
284+
* <p>Disabled by default to preserve the legacy blocking behaviour for
285+
* deployments running on databases without NOWAIT support (MySQL 5.x,
286+
* MariaDB, etc.). Operators on MySQL 8.0+, PostgreSQL or Oracle can opt
287+
* in to fast-fail acquisition.
288+
*/
289+
boolean DEFAULT_DISTRIBUTED_LOCK_DB_NOWAIT_ENABLED = false;
290+
281291
/**
282292
* The constant DEFAULT_TM_COMMIT_RETRY_COUNT.
283293
*/

core/src/main/java/org/apache/seata/core/store/db/sql/distributed/lock/DistributedLockSql.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ public interface DistributedLockSql {
2727
*/
2828
String getSelectDistributeForUpdateSql(String distributedLockTable);
2929

30+
/**
31+
* Get the select distribute lock sql with NOWAIT semantics for fast-fail
32+
* acquisition. Dialects that do not support NOWAIT should fall back to the
33+
* regular {@link #getSelectDistributeForUpdateSql(String)} so the locker
34+
* keeps the legacy blocking behavior.
35+
*
36+
* @param distributedLockTable the table name of the distribute lock table
37+
* @return the sql with NOWAIT clause when supported, otherwise the regular SELECT FOR UPDATE
38+
* @since 2.5.0
39+
*/
40+
default String getSelectDistributeForUpdateNoWaitSql(String distributedLockTable) {
41+
return getSelectDistributeForUpdateSql(distributedLockTable);
42+
}
43+
3044
/**
3145
* Get insert distribute lock sql
3246
* @param distributedLockTable the table name of the distribute lock table
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.seata.core.store.db.sql.distributed.lock;
18+
19+
import org.apache.seata.common.loader.LoadLevel;
20+
import org.apache.seata.core.constants.ServerTableColumnsName;
21+
22+
/**
23+
* MySQL specific {@link DistributedLockSql} that supports NOWAIT (since MySQL 8.0).
24+
*
25+
* <p>The {@code FOR UPDATE NOWAIT} clause makes the database raise an error
26+
* (MySQL error code 3572 / SQLState HY000) instead of blocking when the
27+
* requested row is already locked. Older MySQL releases (5.7 / 5.6) and
28+
* MariaDB do not understand the NOWAIT keyword; for those deployments,
29+
* stick with the default {@link BaseDistributedLockSql} dialect.
30+
*
31+
* @since 2.5.0
32+
*/
33+
@LoadLevel(name = "mysql")
34+
public class MysqlDistributedLockSql extends BaseDistributedLockSql {
35+
36+
private static final String SELECT_FOR_UPDATE_NOWAIT_SQL =
37+
"SELECT " + ALL_COLUMNS + " FROM " + DISTRIBUTED_LOCK_TABLE_PLACE_HOLD + " WHERE "
38+
+ ServerTableColumnsName.DISTRIBUTED_LOCK_KEY + " = ? FOR UPDATE NOWAIT";
39+
40+
@Override
41+
public String getSelectDistributeForUpdateNoWaitSql(String distributedLockTable) {
42+
return SELECT_FOR_UPDATE_NOWAIT_SQL.replace(DISTRIBUTED_LOCK_TABLE_PLACE_HOLD, distributedLockTable);
43+
}
44+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.seata.core.store.db.sql.distributed.lock;
18+
19+
import org.apache.seata.common.loader.LoadLevel;
20+
import org.apache.seata.core.constants.ServerTableColumnsName;
21+
22+
/**
23+
* Oracle specific {@link DistributedLockSql} that supports NOWAIT.
24+
*
25+
* <p>When the row is locked by another session, Oracle raises {@code ORA-00054}
26+
* ({@code resource busy and acquire with NOWAIT specified or timeout expired}),
27+
* allowing the locker to fast-fail.
28+
*
29+
* @since 2.5.0
30+
*/
31+
@LoadLevel(name = "oracle")
32+
public class OracleDistributedLockSql extends BaseDistributedLockSql {
33+
34+
private static final String SELECT_FOR_UPDATE_NOWAIT_SQL =
35+
"SELECT " + ALL_COLUMNS + " FROM " + DISTRIBUTED_LOCK_TABLE_PLACE_HOLD + " WHERE "
36+
+ ServerTableColumnsName.DISTRIBUTED_LOCK_KEY + " = ? FOR UPDATE NOWAIT";
37+
38+
@Override
39+
public String getSelectDistributeForUpdateNoWaitSql(String distributedLockTable) {
40+
return SELECT_FOR_UPDATE_NOWAIT_SQL.replace(DISTRIBUTED_LOCK_TABLE_PLACE_HOLD, distributedLockTable);
41+
}
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.seata.core.store.db.sql.distributed.lock;
18+
19+
import org.apache.seata.common.loader.LoadLevel;
20+
import org.apache.seata.core.constants.ServerTableColumnsName;
21+
22+
/**
23+
* PostgreSQL specific {@link DistributedLockSql} that supports NOWAIT.
24+
*
25+
* <p>When the row is locked by another session, PostgreSQL raises a
26+
* {@code lock_not_available} error (SQLSTATE {@code 55P03}) instead of
27+
* blocking, allowing the locker to fast-fail.
28+
*
29+
* @since 2.5.0
30+
*/
31+
@LoadLevel(name = "postgresql")
32+
public class PostgresqlDistributedLockSql extends BaseDistributedLockSql {
33+
34+
private static final String SELECT_FOR_UPDATE_NOWAIT_SQL =
35+
"SELECT " + ALL_COLUMNS + " FROM " + DISTRIBUTED_LOCK_TABLE_PLACE_HOLD + " WHERE "
36+
+ ServerTableColumnsName.DISTRIBUTED_LOCK_KEY + " = ? FOR UPDATE NOWAIT";
37+
38+
@Override
39+
public String getSelectDistributeForUpdateNoWaitSql(String distributedLockTable) {
40+
return SELECT_FOR_UPDATE_NOWAIT_SQL.replace(DISTRIBUTED_LOCK_TABLE_PLACE_HOLD, distributedLockTable);
41+
}
42+
}

core/src/main/resources/META-INF/services/org.apache.seata.core.store.db.sql.distributed.lock.DistributedLockSql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@
1616
#
1717
org.apache.seata.core.store.db.sql.distributed.lock.BaseDistributedLockSql
1818
org.apache.seata.core.store.db.sql.distributed.lock.BaseDistributedLockSqlServer
19+
org.apache.seata.core.store.db.sql.distributed.lock.MysqlDistributedLockSql
20+
org.apache.seata.core.store.db.sql.distributed.lock.PostgresqlDistributedLockSql
21+
org.apache.seata.core.store.db.sql.distributed.lock.OracleDistributedLockSql

core/src/test/java/org/apache/seata/core/store/db/sql/distributed/lock/DistributedLockSqlFactoryTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@ class DistributedLockSqlFactoryTest {
2626
void testGetDistributedLogStoreSqlForMysql() {
2727
DistributedLockSql sql = DistributedLockSqlFactory.getDistributedLogStoreSql("mysql");
2828
assertNotNull(sql);
29-
assertTrue(sql instanceof BaseDistributedLockSql);
29+
assertTrue(sql instanceof MysqlDistributedLockSql);
30+
}
31+
32+
@Test
33+
void testGetDistributedLogStoreSqlForPostgresql() {
34+
DistributedLockSql sql = DistributedLockSqlFactory.getDistributedLogStoreSql("postgresql");
35+
assertNotNull(sql);
36+
assertTrue(sql instanceof PostgresqlDistributedLockSql);
37+
}
38+
39+
@Test
40+
void testGetDistributedLogStoreSqlForOracle() {
41+
DistributedLockSql sql = DistributedLockSqlFactory.getDistributedLogStoreSql("oracle");
42+
assertNotNull(sql);
43+
assertTrue(sql instanceof OracleDistributedLockSql);
3044
}
3145

3246
@Test
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.seata.core.store.db.sql.distributed.lock;
18+
19+
import org.apache.seata.core.constants.ServerTableColumnsName;
20+
import org.junit.jupiter.api.Test;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertFalse;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
/**
27+
* Unit tests for the dialect specific {@link DistributedLockSql} implementations
28+
* that support {@code FOR UPDATE NOWAIT} (issue #7993).
29+
*/
30+
class DistributedLockSqlNoWaitTest {
31+
32+
private static final String TABLE = "distributed_lock";
33+
34+
private static final String SELECT_BASE_SQL = "SELECT " + ServerTableColumnsName.DISTRIBUTED_LOCK_KEY + ","
35+
+ ServerTableColumnsName.DISTRIBUTED_LOCK_VALUE + "," + ServerTableColumnsName.DISTRIBUTED_LOCK_EXPIRE
36+
+ " FROM " + TABLE + " WHERE " + ServerTableColumnsName.DISTRIBUTED_LOCK_KEY + " = ?";
37+
38+
@Test
39+
void testMysqlDialectAppendsNoWait() {
40+
MysqlDistributedLockSql sql = new MysqlDistributedLockSql();
41+
assertEquals(SELECT_BASE_SQL + " FOR UPDATE NOWAIT", sql.getSelectDistributeForUpdateNoWaitSql(TABLE));
42+
// legacy blocking variant must be preserved for backward compatibility
43+
assertEquals(SELECT_BASE_SQL + " FOR UPDATE", sql.getSelectDistributeForUpdateSql(TABLE));
44+
}
45+
46+
@Test
47+
void testPostgresqlDialectAppendsNoWait() {
48+
PostgresqlDistributedLockSql sql = new PostgresqlDistributedLockSql();
49+
assertEquals(SELECT_BASE_SQL + " FOR UPDATE NOWAIT", sql.getSelectDistributeForUpdateNoWaitSql(TABLE));
50+
assertEquals(SELECT_BASE_SQL + " FOR UPDATE", sql.getSelectDistributeForUpdateSql(TABLE));
51+
}
52+
53+
@Test
54+
void testOracleDialectAppendsNoWait() {
55+
OracleDistributedLockSql sql = new OracleDistributedLockSql();
56+
assertEquals(SELECT_BASE_SQL + " FOR UPDATE NOWAIT", sql.getSelectDistributeForUpdateNoWaitSql(TABLE));
57+
assertEquals(SELECT_BASE_SQL + " FOR UPDATE", sql.getSelectDistributeForUpdateSql(TABLE));
58+
}
59+
60+
@Test
61+
void testBaseDialectFallsBackToBlockingForNoWait() {
62+
// BaseDistributedLockSql does not override the NOWAIT method and
63+
// must fall back to the regular FOR UPDATE statement so dialects
64+
// without NOWAIT support keep their previous semantics.
65+
BaseDistributedLockSql sql = new BaseDistributedLockSql();
66+
String noWait = sql.getSelectDistributeForUpdateNoWaitSql(TABLE);
67+
assertEquals(sql.getSelectDistributeForUpdateSql(TABLE), noWait);
68+
assertFalse(noWait.toUpperCase().contains("NOWAIT"));
69+
}
70+
71+
@Test
72+
void testSqlServerDialectFallsBackToBlockingForNoWait() {
73+
// SQL Server uses table hints (WITH (ROWLOCK, UPDLOCK, HOLDLOCK))
74+
// and does not support NOWAIT in this codepath. The default
75+
// implementation should return the regular hinted SELECT.
76+
BaseDistributedLockSqlServer sql = new BaseDistributedLockSqlServer();
77+
String noWait = sql.getSelectDistributeForUpdateNoWaitSql(TABLE);
78+
assertEquals(sql.getSelectDistributeForUpdateSql(TABLE), noWait);
79+
assertTrue(noWait.contains("WITH (ROWLOCK, UPDLOCK, HOLDLOCK)"));
80+
}
81+
}

0 commit comments

Comments
 (0)