在Spring Boot和Hibernate中使用@Transactional出现数据库死锁问题。

huangapple go评论103阅读模式
英文:

Getting database deadlock with @Transactional in spring boot and hibernate

问题

为什么在这段代码中会出现死锁?

我尝试过进行调试,也阅读了很多关于死锁预防的文章,但仍然无法解决。我已经使用了同步机制,根据accountNumber来使代码块在多线程情况下保持线程安全。

我从一个API中获取这个Transaction对象,并且我想根据Transaction对象的内容来对代码进行加锁。Transaction对象包含有关借记/贷记账号号码、金额等信息。

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

在这里,lockedAccount存储了当前被锁定的所有账号,有两个方法用于锁定和解锁accountNumber

DAO / Repository 层。

  1. @Repository
  2. public class TransactionDAOImpl implements TransactionDAO {
  3. // 实体管理器字段
  4. private EntityManager entityManager;
  5. public TransactionDAOImpl() {}
  6. // 设置构造器注入
  7. @Autowired
  8. public TransactionDAOImpl(EntityManager theEntityManager) {
  9. entityManager = theEntityManager;
  10. }
  11. private static final Set<String> lockedAccounts = new HashSet<>();
  12. private void LockAccount(String AccountNumber) throws InterruptedException {
  13. int count = 0;
  14. synchronized (lockedAccounts) {
  15. while (!lockedAccounts.add(AccountNumber)) {
  16. lockedAccounts.wait();
  17. count++;
  18. }
  19. System.out.println(AccountNumber + " 等待了 " + count + " 次,现在我获得了锁");
  20. }
  21. }
  22. private void unLockAccount(String AccountNumber) {
  23. synchronized (lockedAccounts) {
  24. lockedAccounts.remove(AccountNumber);
  25. lockedAccounts.notifyAll();
  26. System.out.println("解锁 " + AccountNumber);
  27. }
  28. }
  29. @Override
  30. public void executeTransaction(Transaction theTransaction) {
  31. // 获取当前的 Hibernate 会话
  32. Session currentSession = entityManager.unwrap(Session.class);
  33. // 按升序锁定两个账号,以避免死锁
  34. // 字典顺序较小的账号应先被锁定
  35. String firstAccount = theTransaction.getDebitAccountNumber();
  36. String secondAccount = theTransaction.getCreditAccountNumber();
  37. // 检查 firstAccount 是否小于 secondAccount,如果不是,则交换值
  38. if (firstAccount.compareTo(secondAccount) > 0) {
  39. firstAccount = theTransaction.getCreditAccountNumber();
  40. secondAccount = theTransaction.getDebitAccountNumber();
  41. }
  42. try {
  43. LockAccount(firstAccount);
  44. try {
  45. LockAccount(secondAccount);
  46. AccountDetail debitAccount = getAccountDetails(currentSession, theTransaction.getDebitAccountNumber());
  47. AccountDetail creditAccount = getAccountDetails(currentSession, theTransaction.getCreditAccountNumber());
  48. if (debitAccount == null || creditAccount == null) // 检查无效的账号号码
  49. {
  50. theTransaction.setStatus("失败,账号未找到");
  51. } else if (debitAccount.getBalance() < theTransaction.getAmount()) // 检查账号余额不足
  52. {
  53. theTransaction.setStatus("失败,账号余额不足");
  54. } else {
  55. // 更新客户账号余额
  56. debitAccount.setBalance(debitAccount.getBalance() - theTransaction.getAmount());
  57. creditAccount.setBalance(creditAccount.getBalance() + theTransaction.getAmount());
  58. // 保存到数据库
  59. currentSession.saveOrUpdate(debitAccount);
  60. currentSession.saveOrUpdate(creditAccount);
  61. // 更新交易状态
  62. theTransaction.setStatus("成功");
  63. }
  64. } catch (InterruptedException e) {
  65. e.printStackTrace();
  66. } finally {
  67. unLockAccount(secondAccount);
  68. }
  69. } catch (InterruptedException e1) {
  70. e1.printStackTrace();
  71. } finally {
  72. unLockAccount(firstAccount);
  73. }
  74. return;
  75. }
  76. private AccountDetail getAccountDetails(Session currentSession, String accountNumber) {
  77. Query<?> query = currentSession.createQuery("from AccountDetail where accountNumber=:accountNumber");
  78. query.setParameter("accountNumber", accountNumber);
  79. AccountDetail accountDetails = (AccountDetail) query.uniqueResult();
  80. return accountDetails;
  81. }
  82. }

关于更多信息,我的数据库中的accountDetails表有三个列:

id(int,主键)

accountNumber(String,唯一)

amount(double)

这是服务层,我在executeTransaction方法上使用了@Transactional注解。

  1. public class TransactionServiceImpl implements TransactionService {
  2. private TransactionDAO theTransactionDAO;
  3. public TransactionServiceImpl() {}
  4. // 构造器注入
  5. @Autowired
  6. public TransactionServiceImpl(TransactionDAO theTransactionDAO) {
  7. this.theTransactionDAO = theTransactionDAO;
  8. }
  9. @Override
  10. @Transactional
  11. public void executeTransaction(Transaction theTransaction) {
  12. theTransactionDAO.executeTransaction(theTransaction);
  13. }
  14. }

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

  1. 2020-08-30 19:09:28.235 WARN 6948 --- [nio-8081-exec-4] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL 错误: 1213, SQL 状态: 40001
  2. 2020-08-30 19:09:28.236 ERROR 6948 --- [nio-8081-exec-4] o.h.engine.jdbc.spi.SqlExceptionHelper : 在尝试获取锁定时发现死锁; 请尝试重新启动事务
  3. 2020-08-30 19:09:28.384 ERROR 6948 --- [nio-8081-exec-4] o.a.c.c.C.[.[.[.[dispatcherServlet] : 在路径为 [/bank] 的上下文中为 servlet [dispatcherServlet] 提供的服务(Servlet.service())抛出了异常 [请求处理失败; 嵌套异常是 org.springframework.dao.CannotAcquireLockException: 无法执行语句; SQL [n/a]; 嵌套异常是 org.hibernate.exception.LockAcquisitionException: 无法执行语句],根本原因如下:
  4. com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: 在尝试获取锁定时发现死锁; 请尝试重新启动事务
  5. at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123) ~[mysql-connector-java-8.0.21.jar:8.0.21]
  6. at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.21.jar:8.0.21]
  7. at com.mysql
  8. <details>
  9. <summary>英文:</summary>
  10. Why I am getting deadlock in this code?
  11. I tried to debug it and also read many article about deadlock prevention but could not get this. I have used synchronization, to make thread safe a block of code on the basis of `accountNumber`.
  12. I am getting this `Transaction` object from an API and I want to lock my code on the basis of what the `Transaction` object contain. `Transaction` object contains info like debit/credit account number, amount etc.
  13. Two threads should not be executed `executeTransaction` method simultaneously if there is any common `accountNumber` between them.
  14. Here, `lockedAccount` is storing all accounts that are currently locked and two methods for locking and unlocking an `accountNumber`.
  15. DAO / Repository layer.

@Repository
public class TransactionDAOImpl implements TransactionDAO {

  1. // define field for entitymanager
  2. private EntityManager entityManager;
  3. public TransactionDAOImpl() {}
  4. // set up constructor injection
  5. @Autowired
  6. public TransactionDAOImpl(EntityManager theEntityManager) {
  7. entityManager = theEntityManager;
  8. }
  9. private static final Set&lt;String&gt; lockedAccounts = new HashSet&lt;&gt;();
  10. private void LockAccount(String AccountNumber) throws InterruptedException {
  11. int count = 0;
  12. synchronized (lockedAccounts) {
  13. while (!lockedAccounts.add(AccountNumber)) {
  14. lockedAccounts.wait();
  15. count++;
  16. }
  17. System.out.println(AccountNumber + &quot; waited for &quot; + count + &quot; times&quot; + &quot; and now i am getting lock&quot;);
  18. }
  19. }
  20. private void unLockAccount(String AccountNumber) {
  21. synchronized (lockedAccounts) {
  22. lockedAccounts.remove(AccountNumber);
  23. lockedAccounts.notifyAll();
  24. System.out.println(&quot;unlocking &quot; + AccountNumber);
  25. }
  26. }
  27. @Override
  28. public void executeTransaction(Transaction theTransaction) {
  29. // System.out.println(theTransaction);
  30. // get the current hibernate session
  31. Session currentSession = entityManager.unwrap(Session.class);
  32. // lock both account in a increasing order to avoid deadlock
  33. // lexicographically lesser account number should be lock first
  34. String firstAccount = theTransaction.getDebitAccountNumber();
  35. String secondAccount = theTransaction.getCreditAccountNumber();
  36. // check firstAccount is lesser or greater then second account,if not then swap
  37. // value
  38. if (firstAccount.compareTo(secondAccount) &gt; 0) {
  39. firstAccount = theTransaction.getCreditAccountNumber();
  40. secondAccount = theTransaction.getDebitAccountNumber();
  41. }
  42. try {
  43. LockAccount(firstAccount);
  44. try {
  45. LockAccount(secondAccount);
  46. AccountDetail debitAccount = getAccountDetails(currentSession, theTransaction.getDebitAccountNumber());
  47. AccountDetail creditAccount = getAccountDetails(currentSession,
  48. theTransaction.getCreditAccountNumber());
  49. if (debitAccount == null || creditAccount == null) // check invalid accountNumber
  50. {
  51. theTransaction.setStatus(&quot;failed,account not found&quot;);
  52. } else if (debitAccount.getBalance() &lt; theTransaction.getAmount()) // check insufficient account balance
  53. {
  54. theTransaction.setStatus(&quot;failed,insufficient account balance&quot;);
  55. } else {
  56. // update custmer accout balance
  57. debitAccount.setBalance(debitAccount.getBalance() - theTransaction.getAmount());
  58. creditAccount.setBalance(creditAccount.getBalance() + theTransaction.getAmount());
  59. // save to database
  60. currentSession.saveOrUpdate(debitAccount);
  61. currentSession.saveOrUpdate(creditAccount);
  62. // update status of transacion
  63. theTransaction.setStatus(&quot;successful&quot;);
  64. }
  65. } catch (InterruptedException e) {
  66. e.printStackTrace();
  67. } finally {
  68. unLockAccount(secondAccount);
  69. }
  70. } catch (InterruptedException e1) {
  71. e1.printStackTrace();
  72. } finally {
  73. unLockAccount(firstAccount);
  74. }
  75. return;
  76. }
  77. private AccountDetail getAccountDetails(Session currentSession, String accountNumber) {
  78. Query&lt;?&gt; query = currentSession.createQuery(&quot;from AccountDetail where accountNumber=:accountNumber&quot;);
  79. query.setParameter(&quot;accountNumber&quot;, accountNumber);
  80. AccountDetail accountDetails = (AccountDetail) query.uniqueResult();
  81. return accountDetails;
  82. }

}

  1. for more information ,
  2. my **accountDetails** table in database have three columns,
  3. **id(int,primary key)**
  4. **AccountNumber(String,unique)**
  5. **amount(double)**
  6. this is Service layer
  7. where i am using `@Transactional` annotation for `executeTransaction` method.

public class TransactionServiceImpl implements TransactionService {

  1. private TransactionDAO theTransactionDAO;
  2. public TransactionServiceImpl() {}
  3. //constructor injection
  4. @Autowired
  5. public TransactionServiceImpl(TransactionDAO theTransactionDAO)
  6. {
  7. this.theTransactionDAO= theTransactionDAO;
  8. }
  9. @Override
  10. @Transactional
  11. public void executeTransaction(Transaction theTransaction) {
  12. theTransactionDAO.executeTransaction(theTransaction);
  13. }

}

  1. but i am getting database deadlock in this code.
  2. below is my error.

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)

  1. </details>
  2. # 答案1
  3. **得分**: 1
  4. 假设有两笔账务交易(`debitAccount`,`creditAccount`):**AT1**(1,2)**AT2**(2,1)。我们有Java锁(**JL**)和数据库锁(**DBL**)。在以下情况下,将发生死锁。

+------+---------------------+---------------------+-----------------------------------------------------+
| 步骤 | AT1 状态 | AT2 状态 | 备注 |
+------+---------------------+---------------------+-----------------------------------------------------+
| 1 | 获取 JL | 等待 JL | |
+------+---------------------+---------------------+-----------------------------------------------------+
| 2 | 释放 JL | 获取 JL | AT1 的 saveOrUpdate 可能不会刷新到数据库, |
| | | | 因此数据库锁可能在此时尚未被获取 |
+------+---------------------+---------------------+-----------------------------------------------------+
| 3 | 刷新 debitAccount | 刷新 debitAccount | AT1 为账户1获取了 DB 锁, |
| | saveOrUpdate | saveOrUpdate | AT2 为账户2获取了 DB 锁 |
+------+---------------------+---------------------+-----------------------------------------------------+
| 4 | AT1 的 DBL 账户1 | AT2 的 DBL 账户2 | |
+------+---------------------+---------------------+-----------------------------------------------------+
| 5 | 刷新 creditAccount | 刷新 creditAccount | AT1 为账户2获取了 DBL, |
| | saveOrUpdate | saveOrUpdate | AT2 为账户1获取了 DBL,发生死锁 |
+------+---------------------+---------------------+-----------------------------------------------------+

  1. 还请注意:
  2. 1. 数据库锁在更新语句在刷新时获取。
  3. 2. 数据库锁在事务提交/回滚时释放。
  4. <details>
  5. <summary>英文:</summary>
  6. Suppose there are two Account Transactions(`debitAccount`,`creditAccount`): **AT1**(1,2) **AT2**(2,1). And we have Java Lock (**JL**) and Database Lock (**DBL**). In following scenario, deadlock will occur.

+------+---------------------+---------------------+-----------------------------------------------------+
| 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. Please also note that
  2. 1. Database lock is acquired in update statement when the statement is flushed.
  3. 2. Database lock is released when transaction commit/rollback.
  4. </details>

huangapple
  • 本文由 发表于 2020年9月2日 13:36:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/63699297.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定