需要帮助了解SELECT的行为..FOR UPDATE导致死锁



我有两个并发事务执行这段代码(为了便于说明而简化):

@Transactional
public void deleteAccounts() {
    List<User> users = em.createQuery("select u from User", User.class)
                         .setLockMode(LockModeType.PESSIMISTIC_WRITE)
                         .getResultList();
    for (User user : users) {
        em.remove(user);
    }
}

我的理解是,其中一个事务,比如事务A,应该首先执行SELECT,锁定它需要的所有行,然后继续执行DELETEs,而另一个事务应该等待A的提交,然后再执行SELECT。但是,此代码处于死锁状态。我哪里错了?

USER表可能有很多外键引用它。如果其中任何一个没有索引,Oracle将在从父表删除行时锁定整个子表。如果多个语句同时运行,即使对于不同的用户,相同的子表也会被锁定。由于这些递归操作的顺序无法控制,因此多个会话可能会以不同的顺序锁定相同的资源,从而导致死锁。

有关更多信息,请参阅概念手册中的本节。

若要解决此问题,请将索引添加到任何未编制索引的外键。如果列名是标准的,那么像这样的脚本可以帮助您找到潜在的候选者:

--Find un-indexed foreign keys.
--
--Foreign keys.
select owner, table_name
from dba_constraints
where r_constraint_name = 'USER_ID_PK'
    and r_owner = 'THE_SCHEMA_NAME'
minus
--Tables with an index on the relevant column.
select table_owner, table_name
from dba_ind_columns
where column_name = 'USER_ID';

当您使用PESIMISTIC_WRITE JPA时,通常会将其转换为SELECT FOR UPDATE,这会在数据库中生成一个锁,而不是一行所必需的,这取决于数据库和您如何配置锁,默认情况下,锁是按页或块而不是按行进行的,因此请查看您的数据库文档以确认您的数据库是如何生成锁的,您也可以更改它,以便为一行应用锁。当你调用方法deleteAccounts时,它会启动一个新的事务,锁将一直处于活动状态,直到事务提交(或回滚)。在这种情况下,当方法完成时,如果其他事务想要获得相同的锁,它就无法获得,我认为这就是为什么你有死锁的原因,我建议你尝试其他机制,可能是乐观锁,也可能是实体锁。

您可以尝试给定一个超时来获取锁定,因此:

em.createQuery("select u from User", User.class)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setHint("javax.persistence.lock.timeout", 5000)
.getResultList();

我发现了一篇很好的文章,更好地解释了这个错误,它是由数据库引起的:

Oracle自动检测死锁并通过滚动解决它们返回死锁中涉及的一个事务/语句,因此释放由该事务锁定的一组资源/数据。这个回滚的会话将观察到Oracle错误:ORA-00060:等待资源时检测到死锁。Oracle还将生产数据库的UDUMP目录下的跟踪文件中的详细信息。

这些死锁通常是由以下应用程序引起的:在同一事务中包含多个表更新应用程序/事务同时作用于同一个表时间这些多表死锁可以通过将表锁定在所有应用程序/事务中的顺序相同,从而防止死锁条件。

最新更新