[Bug] Global lock batch acquire always fails on Dameng (DM) — LockStoreDataBaseDAO.doAcquireLocks relies on executeBatch().length
Ⅰ. Describe the bug
When the Seata Server uses DB store mode with a Dameng (DM) database, any branch transaction that needs to acquire two or more row locks in a single BranchRegister always fails to acquire the global lock, even though the lock_table is empty and there is no real conflict.
The client side gets:
io.seata.rm.datasource.exec.LockConflictException: get global lock fail, xid:..., lockKeys:XXX:1;YYY:2
The server side logs:
LockStoreDataBaseDAO : Global lock batch acquire failed, xid ... branchId ... pks [1, 2]
AbstractExceptionHandler : this request cannot acquire global lock ...
A branch that locks only one row works fine. So in practice any business transaction that modifies 2+ rows (master/detail, entity + i18n text, etc.) can never commit, which makes AT mode effectively unusable on DM for real workloads.
Ⅱ. Root cause
org.apache.seata.server.storage.db.lock.LockStoreDataBaseDAO#doAcquireLocks:
protected boolean doAcquireLocks(Connection conn, List<LockDO> lockDOs) throws SQLException {
...
for (LockDO lockDO : lockDOs) {
... ps.addBatch();
}
return ps.executeBatch().length == lockDOs.size(); // <-- here
}
It judges batch success by executeBatch().length == lockDOs.size().
The DM JDBC driver (DmdbPreparedStatement) implements batch execution via executeBatchByRow() and aggregates the per-statement results through ExecuteRetInfo.union(...), so executeBatch() returns an int[] whose length does not equal the number of batched statements.
As a result:
- The lock rows are actually inserted into
lock_table.
- But
executeBatch().length == lockDOs.size() evaluates to false.
doAcquireLocks returns false, the caller does conn.rollback() (the inserted lock rows are discarded), and the branch register is reported as a lock conflict.
Single-row branches take the doAcquireLock (non-batch) path, which does not have this check, so they succeed — which is why the failure only appears for multi-row branches.
Per the JDBC spec, the return value of Statement.executeBatch() is an array of update counts where each element may be a count, SUCCESS_NO_INFO (-2), or EXECUTE_FAILED (-3). Assuming length == number of statements is not guaranteed across drivers; relying on it is the bug.
Ⅲ. How to reproduce
- Seata Server
store.mode=db, store DB = Dameng (DM8).
- Any AT-mode business transaction whose single branch updates/inserts ≥ 2 rows (so the branch reports
lockKey with 2+ pks).
- Branch register fails with
Global lock batch acquire failed against an empty lock_table.
Ⅳ. Expected behavior
The batch lock acquire should succeed when the lock rows are inserted without conflict, regardless of the driver-specific length of the executeBatch() return array. Real conflicts (duplicate row_key) still surface as SQLIntegrityConstraintViolationException and are already handled.
Ⅴ. Environment
- Seata version: 2.6.0 (the same code is present on the current
2.x branch).
- DB store: Dameng (DM8), DM JDBC driver
DmJdbcDriver18.
- The fix is proposed in the accompanying PR.
Ⅵ. Suggested fix
Stop relying on executeBatch().length. Detect failure via Statement.EXECUTE_FAILED instead:
int[] result = ps.executeBatch();
for (int updated : result) {
if (updated == java.sql.Statement.EXECUTE_FAILED) {
return false;
}
}
return true;
This is correct for drivers that return one element per statement and for drivers (like DM) that aggregate the result array, while preserving the existing conflict handling (the catch (SQLIntegrityConstraintViolationException ...) branch).
[Bug] Global lock batch acquire always fails on Dameng (DM) —
LockStoreDataBaseDAO.doAcquireLocksrelies onexecuteBatch().lengthⅠ. Describe the bug
When the Seata Server uses DB store mode with a Dameng (DM) database, any branch transaction that needs to acquire two or more row locks in a single
BranchRegisteralways fails to acquire the global lock, even though thelock_tableis empty and there is no real conflict.The client side gets:
The server side logs:
A branch that locks only one row works fine. So in practice any business transaction that modifies 2+ rows (master/detail, entity + i18n text, etc.) can never commit, which makes AT mode effectively unusable on DM for real workloads.
Ⅱ. Root cause
org.apache.seata.server.storage.db.lock.LockStoreDataBaseDAO#doAcquireLocks:It judges batch success by
executeBatch().length == lockDOs.size().The DM JDBC driver (
DmdbPreparedStatement) implements batch execution viaexecuteBatchByRow()and aggregates the per-statement results throughExecuteRetInfo.union(...), soexecuteBatch()returns anint[]whose length does not equal the number of batched statements.As a result:
lock_table.executeBatch().length == lockDOs.size()evaluates to false.doAcquireLocksreturnsfalse, the caller doesconn.rollback()(the inserted lock rows are discarded), and the branch register is reported as a lock conflict.Single-row branches take the
doAcquireLock(non-batch) path, which does not have this check, so they succeed — which is why the failure only appears for multi-row branches.Ⅲ. How to reproduce
store.mode=db, store DB = Dameng (DM8).lockKeywith 2+ pks).Global lock batch acquire failedagainst an emptylock_table.Ⅳ. Expected behavior
The batch lock acquire should succeed when the lock rows are inserted without conflict, regardless of the driver-specific length of the
executeBatch()return array. Real conflicts (duplicaterow_key) still surface asSQLIntegrityConstraintViolationExceptionand are already handled.Ⅴ. Environment
2.xbranch).DmJdbcDriver18.Ⅵ. Suggested fix
Stop relying on
executeBatch().length. Detect failure viaStatement.EXECUTE_FAILEDinstead:This is correct for drivers that return one element per statement and for drivers (like DM) that aggregate the result array, while preserving the existing conflict handling (the
catch (SQLIntegrityConstraintViolationException ...)branch).