避免死锁的通常建议是始终以相同的顺序锁定资源。但是,对于高度满足的Oracle数据库中的行锁,您将如何实现这一点?
要了解我的意思,请考虑以下示例。一个非常简单的DAO来处理银行账户:
@Component
public class AccountDao {
@Resource
private DataSource dataSource;
public void withdraw(String account, int amount) {
modifyBalance(account, -amount);
}
public void deposit(String account, int amount) {
modifyBalance(account, amount);
}
private void modifyBalance(String account, int amount) {
try {
Connection connection = DataSourceUtils.getConnection(dataSource);
PreparedStatement statement = connection
.prepareStatement("update account set balance = balance + ? where holder = ?");
statement.setInt(1, amount);
statement.setString(2, account);
statement.execute();
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
要在两个帐户之间执行转帐,有某种InternalBankTransfer
类具有转帐方式:
public void transfer(String from, String to, int amount) {
// start transaction
accountDao.withDraw(from, amount);
accountDao.deposit(to, amount);
// commit transaction
}
通常情况下这很好。但是,假设我们有两个人同时启动转账。让我们说安妮想要在鲍勃想要将50转移到安妮的同时向鲍勃转移100美元。所以在一个线程中,Anne调用transfer("Anne", "Bob", 100)
,而另一个Bob调用transfer("Bob", "Anne", 50)
。如果执行顺序如下,则此代码容易受到死锁的影响:
T1: accountDao.withDraw("Anne", 100);
T2: accountDao.withDraw("Bob", 50);
T1: accountDao.deposit("Bob", 100);
T2: accountDao.deposit("Anne", 50); // BAM! ORA-00060: deadlock detected while waiting for resource
我承认在我开始在真实应用程序中看到死锁之前我根本没有考虑过这个问题。我天真的看法是事务隔离类型自动处理这个问题。甲骨文表示,这是由于应用程序设计不佳造成的。但在这种情况下,什么是好的设计?我需要select for update
我计划更新的所有内容吗?如果这是一个涉及更新几个表的巨大交易怎么办?我是否应该设计使死锁不可能或只是最小化它们并接受they are a fact of life?
答案 0 :(得分:4)
我认为这是生活中的事实(而且应该只有高并发和热点数据才会发生。)
如果你想实现锁定顺序,那么是的,你需要重写代码以按照预先确定的顺序锁定或更新帐户(首先是Anne,然后是Bob)。但是,对于复杂的交易,这是不可行的。如果它只发生在一些热点行,也许你可以使用锁定顺序(并保持原样),并顺其自然。
或者使用较少粒度的锁,但这会破坏您的并发性。
在您的情况下,您只需重试已中止的事务即可。如果它经常发生,看起来你的应用程序设计确实存在问题。
以下是a link for a two-phase commit protocol银行帐户转帐。它来自MongoDB wiki,即首先没有行锁和事务的人,但是也可以在RDBMS上实现它,以避免锁争用。那当然是一个相当激进的应用程序重新设计。我首先尝试其他一切(重试,粗锁,人为降低并发级别,批处理)。
答案 1 :(得分:4)
上述设计存在一些问题。
即使您询问死锁,我也觉得有必要另外写下其他错误的问题恕我直言,他们可能会在将来免除您的麻烦。
在您的设计中,我看到的第一个问题是方法分离: 为了对余额进行修改,您有一种退出方法和存款方法。在每个中,您使用相同的方法“modifyBalance”来执行操作。它的完成方式几乎没有问题:
1- modifyBalance方法每次调用时都会请求一个连接 2-连接很可能已启用自动提交模式,因为您没有将自动提交设置为关闭。
为什么这会有问题? 你正在做的逻辑应该是一个单元。假设你从鲍勃退出50并且它成功了。你有自动提交,并且更改是最终的。现在你试图向安妮存款但它失败了。根据上面的代码,安妮不会得到50,但鲍勃已经失去了他们!所以在这种情况下你需要再次将押金押在bob上并将50返回给他,希望它不会失败或者......无限处理。 因此,这些行动应该在同一笔交易中。无论是退出还是存款都成功了,他们都会得到承诺,或者他们都会失败,一切都会回滚。
它也有问题,因为在自动提交模式下,提交在语句完成或下一次执行发生后发生。如果由于任何原因提交没有发生,那么因为你没有关闭连接(这是另一个问题,因为它没有回到池)并且没有提交发生可能导致死锁如果在在第一笔交易中锁定了一行。
因此,我建议您执行以下操作:在传输方法中请求连接,或者将方法撤消并存入方法修改余额本身。
因为在我看来你喜欢这两种方法的想法,我将演示我提到的第一个选项的用法:)
public class AccountDao {
@Resource
private DataSource dataSource;
public void withdraw(String account, int amount,Connection connection) throws SQLException{
modifyBalance(account, -amount);
}
public void deposit(String account, int amount,Connection connection) throws SQLException{
modifyBalance(account, amount);
}
private void modifyBalance(String account, int amount,Connection connection) throws SQLException {
PreparedStatement statement = connection.prepareStatement("update account set balance = balance + ? where holder = ?");
statement.setInt(1, amount);
statement.setString(2, account);
statement.execute();
}
}
并且转移方法变为:
public void transfer(String from, String to, int amount) {
try {
Connection connection = DataSourceUtils.getConnection(dataSource);
connection.setAutoCommit(false);
accountDao.withDraw(from, amount,connection);
accountDao.deposit(to, amount,connection);
}
catch (SQLException e) {
if (connection!=null)
connection.rollback();
throw new RuntimeException(e);
}
finally {
if (connection!=null){
connection.commit();
connection.close();
}
}
}
现在要么两个动作都成功,要么两者都回滚。此外,当在一行上发布更新时,尝试更新该行的其他事务将等待它完成,然后才能继续。回滚或提交确保释放行级锁定。
现在,上面是对更好设计的解释,以保持逻辑操作和数据的正确性。但它不会解决你的锁定问题!!!!这是可能发生的事情的一个例子:
假设第一个线程试图退出bob。
状态:由t1锁定的行bob
此时,线程二退出安妮
状态:由线程2锁定行安妮
现在主题1想要存入到安妮
状态:线程1看到行安妮被锁定所以它等待锁定被释放所以它可以进行更新:线程1实际上正在等待线程调整完成更新并提交或回滚锁定被释放
现在线程二想要存入bob
状态:bob行被锁定,因此线程2等待其释放
DEADLOCK !!!!!
两个线程正在等待。
那我们该如何解决呢?请查看已发布的答案(我在输入时看到了这些答案)并且请不要接受此答案,但请接受您实际用来防止死锁的答案。我只是想像我一样谈论其他问题,并抱歉这么久。
答案 2 :(得分:2)
在尝试更新之前,您可以在该行上使用SELECT FOR UPDATE NOWAIT
。如果该行已被锁定,您将收到错误(ORA-00054)。等一下,然后重试(*)或抛出异常。
你永远不应该遇到死锁,因为它们很容易被阻止。
(*)在这种情况下,您必须重试整个事务(在回滚之后)以防止出现死锁情况。
答案 3 :(得分:0)
假设撤销和存款是单个数据库事务的一部分,只需按顺序处理帐户就可以相对容易地避免死锁。如果您的应用程序通过借记或贷记较低的帐号来实现转移,然后借记或贷记较高的帐号,您将永远无法通过发出多个并发转移来解锁。从死锁预防的角度来看,只要您对执行该命令保持一致,您执行的命令(尽管它可能对应用程序性能有关)并不重要。