【问题标题】:Getting database deadlock with @Transactional in spring boot and hibernate在spring boot和hibernate中使用@Transactional获取数据库死锁
【发布时间】:2020-09-02 05:36:37
【问题描述】:

为什么我在这段代码中遇到了死锁?

我尝试调试它并阅读了许多关于死锁预防的文章,但无法得到这个。我使用了同步,在accountNumber的基础上使线程安全的代码块。

我从 API 获取此 Transaction 对象,我想根据 Transaction 对象包含的内容锁定我的代码。 Transaction 对象包含借方/贷方帐号、金额等信息。

如果两个线程之间有任何共同的accountNumber,则不应同时执行executeTransaction方法。

这里,lockedAccount 存储了当前锁定的所有帐户以及锁定和解锁accountNumber 的两种方法。

DAO/存储库层。

@Repository
public class TransactionDAOImpl implements TransactionDAO {

    // define field for entitymanager
    private EntityManager entityManager;

    public TransactionDAOImpl() {}
    // set up constructor injection
    @Autowired
    public TransactionDAOImpl(EntityManager theEntityManager) {
        entityManager = theEntityManager;
    }


    private static final Set<String> lockedAccounts = new HashSet<>();

    private void LockAccount(String AccountNumber) throws InterruptedException {
        int count = 0;
        synchronized (lockedAccounts) {
            while (!lockedAccounts.add(AccountNumber)) {
                lockedAccounts.wait();
                count++;
            }
            System.out.println(AccountNumber + " waited for " + count + " times" + " and now i am getting lock");
        }
    }

    private void unLockAccount(String AccountNumber) {
        synchronized (lockedAccounts) {
            lockedAccounts.remove(AccountNumber);
            lockedAccounts.notifyAll();
            System.out.println("unlocking " + AccountNumber);
        }
    }

    
    @Override
    public void executeTransaction(Transaction theTransaction) {
        // System.out.println(theTransaction);
        // get the current hibernate session
        Session currentSession = entityManager.unwrap(Session.class);

        // lock both account in a increasing order to avoid deadlock
        // lexicographically lesser account number should be lock first
        String firstAccount = theTransaction.getDebitAccountNumber();
        String secondAccount = theTransaction.getCreditAccountNumber();
        // check firstAccount is lesser or greater then second account,if not then swap
        // value
        if (firstAccount.compareTo(secondAccount) > 0) {
            firstAccount = theTransaction.getCreditAccountNumber();
            secondAccount = theTransaction.getDebitAccountNumber();
        }
        try {
            LockAccount(firstAccount);
            try {
                LockAccount(secondAccount);
               
                AccountDetail debitAccount = getAccountDetails(currentSession, theTransaction.getDebitAccountNumber());
                AccountDetail creditAccount = getAccountDetails(currentSession,
                        theTransaction.getCreditAccountNumber());

                if (debitAccount == null || creditAccount == null) // check invalid accountNumber
                {
                    theTransaction.setStatus("failed,account not found");
                } else if (debitAccount.getBalance() < theTransaction.getAmount()) // check insufficient account balance
                {
                    theTransaction.setStatus("failed,insufficient account balance");
                } else {
                    // update custmer accout balance
                    debitAccount.setBalance(debitAccount.getBalance() - theTransaction.getAmount());
                    creditAccount.setBalance(creditAccount.getBalance() + theTransaction.getAmount());
                   
                    // save to database
                    currentSession.saveOrUpdate(debitAccount);
                    currentSession.saveOrUpdate(creditAccount);

                    // update status of transacion
                    theTransaction.setStatus("successful");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unLockAccount(secondAccount);
            }
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        } finally {
            unLockAccount(firstAccount);
        }
        return;
    }
     private AccountDetail getAccountDetails(Session currentSession, String accountNumber) {

        Query<?> query = currentSession.createQuery("from AccountDetail where accountNumber=:accountNumber");
        query.setParameter("accountNumber", accountNumber);
        AccountDetail accountDetails = (AccountDetail) query.uniqueResult();
        return accountDetails;
    }
}
    

更多信息, 我在数据库中的 accountDetails 表有三列,

id(int,primary key)

帐号(字符串,唯一)

金额(双倍)

这是服务层 我在哪里使用 @Transactional 注释 executeTransaction 方法。

public class TransactionServiceImpl implements TransactionService {

    private TransactionDAO theTransactionDAO;
    
    public TransactionServiceImpl() {}
    
    //constructor injection
    @Autowired
    public TransactionServiceImpl(TransactionDAO theTransactionDAO)
    {
        this.theTransactionDAO= theTransactionDAO;
    }
    
    @Override
    @Transactional
    public void executeTransaction(Transaction theTransaction) {
        theTransactionDAO.executeTransaction(theTransaction);
    }
}

但是我在这段代码中遇到了数据库死锁。 以下是我的错误。

2020-08-30 19:09:28.235  WARN 6948 --- [nio-8081-exec-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
2020-08-30 19:09:28.236 ERROR 6948 --- [nio-8081-exec-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
2020-08-30 19:09:28.384 ERROR 6948 --- [nio-8081-exec-4] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/bank] threw exception [Request processing failed; nested exception is org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement] with root cause

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123) ~[mysql-connector-java-8.0.21.jar:8.0.21]
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.21.jar:8.0.21]
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.21.jar:8.0.21]
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.21.jar:8.0.21]
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1092) 

【问题讨论】:

  • 嗨@samabcde 你能详细说明一下吗,我的外部锁定会干扰数据库锁定吗??
  • 您可以查看mysql错误日志以获取更多详细信息。
  • Hii @talex 我试着调试它,但实际上我不知道为什么这里会发生死锁
  • 您能在日志中找到错误吗?如果是这样,您可以将其添加到您的问题中吗?

标签: java multithreading hibernate concurrency


【解决方案1】:

假设有两个账户交易(debitAccount,creditAccount):AT1(1,2) AT2(2,1)。我们有 Java 锁 (JL) 和数据库锁 (DBL)。在以下情况下,会发生死锁。

+------+---------------------+---------------------+-----------------------------------------------------+  
| Step | AT1 State           | AT2 State           | Remark                                              |  
+------+---------------------+---------------------+-----------------------------------------------------+  
| 1    | get JL              | wait JL             |                                                     |  
+------+---------------------+---------------------+-----------------------------------------------------+  
| 2    | release JL          | get JL              | AT1 saveOrUpdate may not flush to database,         |  
|      |                     |                     | hence database lock may not be acquired this moment |  
+------+---------------------+---------------------+-----------------------------------------------------+  
| 3    | flush debitAccount  | flush debitAccout   | AT1 acquire DB lock for account 1,                  |  
|      | saveOrUpdate        | saveOrUpdate        | AT2 acquire DB lock for account 2                   |  
+------+---------------------+---------------------+-----------------------------------------------------+  
| 4    | AT1 DBL account 1   | AT2 DBL account 2   |                                                     |  
+------+---------------------+---------------------+-----------------------------------------------------+  
| 5    | flush creditAccount | flush creditAccount | AT1 acquire DBL for account 2,                      |  
|      | saveOrUpdate        | saveOrUpdate        | AT2 acquire DBL for account 1, Deadlock             |  
+------+---------------------+---------------------+-----------------------------------------------------+  

另请注意

  1. 刷新语句时,在更新语句中获取数据库锁。
  2. 事务提交/回滚时释放数据库锁。

【讨论】:

    猜你喜欢
    • 2022-01-12
    • 2011-12-18
    • 2021-05-22
    • 1970-01-01
    • 2019-11-17
    • 1970-01-01
    • 2021-07-11
    • 2011-05-10
    • 2016-10-21
    相关资源
    最近更新 更多