为什么我的JPA与Oracle悲观锁定不起作用?

12

我正在尝试为在不同 JBoss 节点上运行的 Cron 作业实现一些信号量。 我尝试使用数据库(Oracle 11g)作为锁定机制,使用一个表来同步不同节点中的 cron 作业。 这个表非常简单:

CREATE TABLE SYNCHRONIZED_CRON_JOB_TASK
(
   ID            NUMBER(10)           NOT NULL,
   CRONJOBTYPE   VARCHAR2(255 Byte),
   CREATIONDATE  TIMESTAMP(6)         NOT NULL,
   RUNNING       NUMBER(1)
);

ALTER TABLE SYNCHRONIZED_CRON_JOB_TASK
   ADD CONSTRAINT PK_SYNCHRONIZED_CRON_JOB_TASK
   PRIMARY KEY (ID); 

因此,当作业开始时,它会在表中搜索其cronjobtype的条目,并检查它是否已经在运行。如果没有,则更新该条目并将运行标志设置为true。这个第一次查询是使用Hibernate和悲观锁定的JPA CriteriaApi进行的。

query.setLockMode(javax.persistence.LockModeType.PESSIMISTIC_WRITE);

所有这些操作都在一个事务中完成。

当一个进程运行时,它所做的查询如下:

[Server:server-two] 10:38:00,049 INFO  [stdout] (scheduler-2) 2015-04-30 10:38:00,048 WARN  (Loader.java:264) - HHH000444: Encountered request for locking however dialect reports that database prefers locking be done in a separate select (follow-on locking); results will be locked after initial query executes
[Server:server-two] 10:38:00,049 INFO  [stdout] (scheduler-2) Hibernate: select * from ( select distinct synchroniz0_.id as id1_127_, synchroniz0_.creationDate as creation2_127_, synchroniz0_.running as running3_127_, synchroniz0_.CRONJOBTYPE as CRONJOBT4_127_ from SYNCHRONIZED_CRON_JOB_TASK synchroniz0_ where synchroniz0_.CRONJOBTYPE=? ) where rownum <= ?
[Server:server-two] 10:38:00,053 INFO  [stdout] (scheduler-2) Hibernate: select id from SYNCHRONIZED_CRON_JOB_TASK where id =? for update
[Server:server-two] 10:38:00,056 INFO  [stdout] (scheduler-2) Hibernate: update SYNCHRONIZED_CRON_JOB_TASK set creationDate=?, running=?, CRONJOBTYPE=? where id=?

这个警告没有问题,你可以看到先是一个选择操作,然后是一个选择更新操作,所以Oracle应该会阻止对此行的其他选择操作。 但问题在于,查询并没有被阻止,因此两个任务可以进入并进行选择和更新操作而没有问题。锁定没有起作用,如果我们同时运行两个cron作业,就会发现这一点:

[Server:server-one] 10:38:00,008 INFO  [stdout] (scheduler-3) 2015-04-30 10:38:00,008 WARN  (Loader.java:264) - HHH000444: Encountered request for locking however dialect reports that database prefers locking be done in a separate select (follow-on locking); results will be locked after initial query executes
[Server:server-two] 10:38:00,008 INFO  [stdout] (scheduler-2) 2015-04-30 10:38:00,008 WARN  (Loader.java:264) - HHH000444: Encountered request for locking however dialect reports that database prefers locking be done in a separate select (follow-on locking); results will be locked after initial query executes
[Server:server-two] 10:38:00,009 INFO  [stdout] (scheduler-2) Hibernate: select * from ( select distinct synchroniz0_.id as id1_127_, synchroniz0_.creationDate as creation2_127_, synchroniz0_.running as running3_127_, synchroniz0_.CRONJOBTYPE as CRONJOBT4_127_ from SYNCHRONIZED_CRON_JOB_TASK synchroniz0_ where synchroniz0_.CRONJOBTYPE=? ) where rownum <= ?
[Server:server-one] 10:38:00,009 INFO  [stdout] (scheduler-3) Hibernate: select * from ( select distinct synchroniz0_.id as id1_127_, synchroniz0_.creationDate as creation2_127_, synchroniz0_.running as running3_127_, synchroniz0_.CRONJOBTYPE as CRONJOBT4_127_ from SYNCHRONIZED_CRON_JOB_TASK synchroniz0_ where synchroniz0_.CRONJOBTYPE=? ) where rownum <= ?
[Server:server-two] 10:38:00,013 INFO  [stdout] (scheduler-2) Hibernate: select id from SYNCHRONIZED_CRON_JOB_TASK where id =? for update
[Server:server-one] 10:38:00,014 INFO  [stdout] (scheduler-3) Hibernate: select id from SYNCHRONIZED_CRON_JOB_TASK where id =? for update
[Server:server-two] 10:38:00,016 INFO  [stdout] (scheduler-2) 2015-04-30 10:38:00,015 DEBUG (SynchronizedCronJobService.java:65) - Task read SynchronizedCronJobTask [id=185, type=AlertMailTaskExecutor, creationDate=2015-04-25 07:11:33.0, running=false]
[Server:server-two] 10:38:00,018 INFO  [stdout] (scheduler-2) Hibernate: update SYNCHRONIZED_CRON_JOB_TASK set creationDate=?, running=?, CRONJOBTYPE=? where id=?
[Server:server-one] 10:38:00,022 INFO  [stdout] (scheduler-3) 2015-04-30 10:38:00,022 DEBUG (SynchronizedCronJobService.java:65) - Task read SynchronizedCronJobTask [id=185, type=AlertMailTaskExecutor, creationDate=2015-04-25 07:11:33.0, running=false]
[Server:server-one] 10:38:00,024 INFO  [stdout] (scheduler-3) Hibernate: update SYNCHRONIZED_CRON_JOB_TASK set creationDate=?, running=?, CRONJOBTYPE=? where id=?

我尝试在一个 SQL 工具(SQLWorkbenchJ)上使用两个连接进行 select for update 操作,阻塞功能在此工具内正常工作。但是,如果我在 SQL 工具上使用 select for update 并启动 cron 作业,则它们不会被阻止并且可以正常运行。

我认为问题出在 JPA、Hibernate 或 Oracle 驱动程序中,但我不确定。你有任何想法吗?我应该使用其他策略吗? 提前致谢。


1
你是否在JBOSS中实现了“主动”等待?当你将连接返回到连接池时,JBOSS会对其发出ROLLBACK命令,所有锁都会丢失。此外,你的cron作业必须使用SELECT FOR UPDATE,Oracle中纯读取操作永远不会被阻塞。 - ibre5041
我不确定您所说的“主动等待”是什么意思,我没有使用任何JBOSS特殊功能。我希望等待功能是在数据库选择操作上完成的。 - Ricardo Vila
我认为这不是关于悲观锁定,而是关于交易的一般性问题。即使是纯单个UPDATE也可以工作,只要事务保持打开状态并且连接保持在线程中。在JPA中,当代码离开事务边界时,会发出回滚指令并且所有锁都会丢失。 - ibre5041
Ibre,我面临的问题是cron作业启动事务,使用“for update”读取正确的行,如果数据显示其他cron作业正在运行,则不执行任何操作,但关闭事务;否则,它会更新该行并将其设置为正在运行,然后关闭事务。我认为这种方法很好,但也许不是实现我所需求的最佳方式。 - Ricardo Vila
3个回答

4

最终,我设法使它工作,但做了一些修改。想法是使用LockModeType.PESSIMISTIC_FORCE_INCREMENT而不是PESSIMISTIC_WRITE。使用这种锁定模式,Cron Jobs的表现如下:

  1. 当第一个任务进行更新选择时,一切都按预期进行,但对象上的版本会更改。
  2. 如果另一个任务在第一个任务仍在其事务中进行选择,则JPA会发出OptimisticLockException,因此如果捕获该异常,则可以确定它是由于读取锁定而引发的。

这种解决方案有各种对应物:

  1. SynchronizedCronJobTask必须具有版本字段,并使用@Version进行版本控制
  2. 您需要处理OptimisticLockException,并且应该在事务性服务方法之外捕获它,以便在发生锁定时进行回滚。
  3. 在我看来,这不是一种优雅的解决方案,比简单等待先前完成的任务的锁要糟糕得多。

1
我可以确认Ricardo的观察结果。我已经使用H2数据库测试了几种锁模式,所有模式都按预期工作。悲观锁模式与Oracle数据库的组合均未正常工作。我没有尝试过乐观锁定,但令人惊讶的是,有一种锁定模式根本无法与顶级数据库配合使用。

我已经在这里更详细地发布了我的问题:https://dev59.com/Eanka4cB1Zd3GeqPKTQg - Jörg Vollmer

0

将锁定模式设置为PESSIMISTIC_READ,因为您需要第二个服务器在更改数据之前了解第一个服务器的更改。


1
我尝试了PESSIMISTIC_READ,但它没有起作用。此外,对于这种情况,正确的锁定模式是PESSIMISTIC_WRITE,因为它会阻止读取和更新。使用PESSIMISTIC_READ只会阻止更新。 - Ricardo Vila
你犯了一个错误。完全相反的是:PESSIMISTIC_READ锁定读和写,但PESSIMISTIC_WRITE只能写入。 - Alexander Fedyukov
嗨,亚历山大。你确定吗?在这个线程https://dev59.com/3HI-5IYBdhLWcg3w0cGG中,Joseph的回答说:- PESSIMISTIC_READ. ... 这种锁定模式不会阻止其他事务读取数据。然而,我尝试使用PESSIMISTIC_READ但没有成功。也许我漏掉了什么。 - Ricardo Vila
不确定了。抱歉。又搞混了。 - Alexander Fedyukov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接