【发布时间】:2020-05-17 23:08:48
【问题描述】:
我试图通过一个简单的银行汇款示例来理解悲观锁。
我相信这种说法会导致死锁
BEGIN TRANSACTION
UPDATE BankAccount SET balance = balance - amount where id = 123;
UPDATE BankAccount SET balance = balance + amount where id = 456;
COMMIT;
而且我相信这种说法也会导致死锁
BEGIN TRANSACTION
SELECT BankAccount wHERE id = 123 FOR UPDATE; // Statement #1
SELECT BankAccount WHERE id = 456 FOR UPDATE; // Statement #2
// perform some logics
UPDATE BankAccount SET balance = balance - amount where id = 123;
UPDATE BankAccount SET balance = balance + amount where id = 456;
COMMIT;
这是因为如果有 2 个并发交易 T1 和 T2,T1 可以使用 Statement #1 锁定第一个帐户,而 T2 可以使用 Statement #2 锁定第二个帐户以死锁结束(如果我错了,请纠正我)
现在我尝试了以下事务,它也导致死锁,但我不明白为什么!
BEGIN TRANSACTION
SELECT BankAccount wHERE id IN (123, 456) FOR UPDATE; // Statement #1
// perform some logics
UPDATE BankAccount SET balance = balance - amount where id = 123;
UPDATE BankAccount SET balance = balance + amount where id = 456;
COMMIT;
注意事项:
- 我已经在 Java 11 环境中使用 JDBC 尝试了所有这些事务。
- 我正在使用多线程来模拟对数据库的并发访问。每次汇款由 1 个单线程进行,该线程随机选择 2 个不同的帐户进行汇款。
这是 StackTrace:
org.postgresql.util.PSQLException: ERROR: deadlock detected
Détail : Process 6596 waits for ExclusiveLock on tuple (314,24) of relation 321198 of database 321194; blocked by process 6643.
Process 6643 waits for ShareLock on transaction 326566; blocked by process 6637.
Process 6637 waits for ShareLock on transaction 326569; blocked by process 6574.
Process 6574 waits for ExclusiveLock on tuple (314,24) of relation 321198 of database 321194; blocked by process 6596.
Indice : See server log for query details.
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2533)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2268)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:313)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:448)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:369)
at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:159)
at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:125)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at com.mssmfactory.service.OptimisticMoneyTransferHandler.transfer(OptimisticMoneyTransferHandler.java:76)
at ConcurrentMoneyTransferHandlerTest.lambda$test$0(ConcurrentMoneyTransferHandlerTest.java:84)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
这是我的转账代码:
public class PessimisticMoneyTransferHandler implements IMoneyTransferHandler {
private IDatabaseConnector iDatabaseConnector;
public void transfer(Long senderId, Long receiverId, Double amount) throws SQLException, NoSuchBankAccountException, InsufficientBalanceException {
try (Connection connection = this.iDatabaseConnector.getConnection()) {
connection.setAutoCommit(false);
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM mssmbank.mssmbank.bankaccounts WHERE id IN (?, ?) FOR UPDATE");
preparedStatement.setLong(1, senderId);
preparedStatement.setLong(2, receiverId);
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
Long firstAccountId = resultSet.getLong("id");
Double firstAccountBalance = resultSet.getDouble("balance");
if (resultSet.next()) {
Double secondAccountBalance = resultSet.getDouble("balance");
boolean isFirstSender = firstAccountId.equals(senderId);
if (isFirstSender && firstAccountBalance < amount) {
connection.rollback();
throw new InsufficientBalanceException();
}
else if (!isFirstSender && secondAccountBalance < amount) {
connection.rollback();
throw new InsufficientBalanceException();
}
preparedStatement = connection.prepareStatement("UPDATE mssmbank.mssmbank.bankaccounts SET balance = balance - ? WHERE id = ?");
preparedStatement.setDouble(1, amount);
preparedStatement.setDouble(2, senderId);
preparedStatement.executeUpdate();
preparedStatement = connection.prepareStatement("UPDATE mssmbank.mssmbank.bankaccounts SET balance = balance + ? WHERE id = ?");
preparedStatement.setDouble(1, amount);
preparedStatement.setDouble(2, receiverId);
preparedStatement.executeUpdate();
connection.commit();
} else throw new NoSuchBankAccountException(receiverId);
} else throw new NoSuchBankAccountException(senderId);
}
}
}
这里是主要代码:
final int numberOfAccount = 10;
final int numberOfTransactions = 150;
List<IBankAccountDetails> bankAccounts = new ArrayList<>(numberOfAccount);
Runnable transaction = () -> {
Random random = new Random();
int emeeterIndex;
int receiverIndex;
do {
emeeterIndex = random.nextInt(numberOfAccount);
receiverIndex = random.nextInt(numberOfAccount);
} while (emeeterIndex == receiverIndex);
IBankAccountDetails emeeterAccount = bankAccounts.get(emeeterIndex);
IBankAccountDetails receiverAccount = bankAccounts.get(receiverIndex);
double amount = random.nextInt((int) (1.5 * emeeterAccount.getAccountBalance()));
try {
this.iMoneyTransferHandler.transfer(emeeterAccount.getAccountId(), receiverAccount.getAccountId(), amount);
} catch (SQLException e) {
e.printStackTrace();
} catch (NoSuchBankAccountException e) {
e.printStackTrace();
} catch (InsufficientBalanceException e) {
e.printStackTrace();
}
};
// ---------------------------------------------------------------------------------------------------
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < numberOfTransactions; i++)
executorService.execute(transaction);
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.SECONDS);
}
【问题讨论】:
-
不确定什么是死锁或只有会话等待锁定?你有错误信息吗?如果是,那是什么?要描述锁定场景或死锁场景,您需要识别数据库会话和时间信息:请编辑您的问题。例如,在您的最后一个场景中,您不能有死锁或锁定场景,因为您只有一个会话和一个事务。
-
抱歉信息太少了。是的,我有一条消息错误。而且我没有提及它,但我正在使用多线程模拟对数据库的多次访问,每个货币交易都发生在 1 个单线程中。我会编辑我的帖子
-
我编辑了,请看一下,告诉我有什么问题吗?如果我对悲观锁的理解是错误的,或者我编程错误并且正在发生死锁
-
你能发布相关时间段的 PostgreSQL 日志吗?你能重现这个问题吗?如果是,您能否仅在测试期间设置
log_statement='all'并发布由死锁中涉及的进程标识符过滤的日志? -
一般来说,如果以不同的顺序获取相同的锁,就会发生死锁。为避免死锁,您可以尝试将 ORDER BY 子句添加到 SELECT... FOR UPDATE 并修改 UPDATE 排序以使排序具有确定性。
标签: postgresql jdbc deadlock rdbms pessimistic-locking