【问题标题】:Why Pessimistic triggers a deadlock为什么悲观会引发僵局
【发布时间】: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;

注意事项:

  1. 我已经在 J​​ava 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$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


【解决方案1】:

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

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;

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

  • 表银行帐户中 id 123 的行
  • 表银行帐户中 id 456 的行

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

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

【讨论】:

  • 你是个天才!谢谢;) ... ORDER BY 也没有死锁!是的,我故意为一组非常小的帐户生成大量事务来模拟并发访问并强制死锁,但缺少正确的 ORDER BY !最后一个问题,例如,如果我将 numberOfAccount 更改为 2000 并保持相同的 numberOfTransactions(较少争用),那么使用 Optimistic 方法会更有趣吗?
  • 视情况而定。您必须考虑目标是什么:更快的执行?减少僵局或冲突?代码更容易维护?乐观锁定可能更快,但您需要以不同的方式编写代码。我认为这个讨论也很有趣asktom.oracle.com/pls/apex/…(即使对于 Oracle 来说——锁定在 Oracle 中或多或少类似于锁定在 Postgres 中)。
  • 我会说更快的执行标准,因为在悲观方法中不会发生死锁,但在整个事务生命周期中行保持锁定。对于 Optimistic Approach,我只是添加了一个关于行版本的列,并在每次执行 UPDATE 语句时对其进行测试。
猜你喜欢
  • 2020-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-26
  • 1970-01-01
相关资源
最近更新 更多