为什么悲观会引发僵局

Why Pessimistic triggers a deadlock

我试图通过一个简单的银行汇款示例来理解悲观锁。

我相信这个语句会导致死锁

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 可以使用 语句 #1 锁定第一个帐户,而 T2 可以使用 锁定第二个帐户语句 #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;

备注:

  1. 我已经在 Java 11 环境中使用 JDBC 尝试了所有这些交易。
  2. 我正在使用多线程来模拟对数据库的并发访问。每次汇款都由 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[=13=](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);
}

如果 运行 并行,则以下事务不应触发死锁:

BEGIN TRANSACTION
UPDATE BankAccount SET balance = balance - amount where id = 123;
UPDATE BankAccount SET balance = balance + amount where id = 456;
COMMIT;

但是如果 运行 并行执行以下事务会触发死锁:

    BEGIN TRANSACTION
    UPDATE BankAccount SET balance = balance - amount where id = 123;
    UPDATE BankAccount SET balance = balance + amount where id = 456;
    COMMIT;

    BEGIN TRANSACTION
    UPDATE BankAccount SET balance = balance - amount where id = 456;
    UPDATE BankAccount SET balance = balance + amount where id = 123;
    COMMIT;

一般来说,当以不同的顺序获取相同的锁(相同的对象,相同的模式)时,就会发生死锁。在这种情况下,它是对以下对象的独占锁:

  • table 银行账户中 ID 123 的行
  • table 银行账户中 ID 456 的行

在您的 Java 代码中,您通过随机生成银行帐号为非常小的一组帐户生成大量交易。这增加了锁定冲突的风险和死锁的风险,因为如果多个交易 运行 在同一帐户上,锁定可能不会以相同的顺序进行:

  • SELECT ... FOR UPDATE 语句中没有 ORDER BY
  • UPDATE 语句的排序仅基于事务中的帐户角色,并且使得两个不同的事务不会以相同的顺序锁定同一行成为可能(因为在一个事务中,给定的帐户是第一个处理的并在另一笔交易中使用上次处理的同一账户)。