TransactionAttribute 注释 (@REQUIRES_NEW) 被忽略
我遇到了两个独立事务的问题,这些事务以与实际执行顺序相反的顺序刷新到数据库。
这是业务案例:存在 RemoteJob-RemoteJobEvent 一对多关系。每次创建新事件时,都会获取一个时间戳,并将其设置在RemoteJob和RemoteJobEvent的lastModified字段中,并持久化两条记录(一条更新+一条插入)。
代码如下:
class Main {
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void mainMethod(...) {
RemoteJob job = remoteJobDAO.findById(...);
// ...
addEvent(job, EVENT_CODE_10);
// Here the separate transaction should have ended and its results
// permanently visible in the database. We refresh the job then
// to update it with the added event:
remoteJobDAO.refresh(job); // calls EntityManager.refresh()
// ...
boolean result = helper.addEventIfNotThere(job);
}
// Annotation REQUIRES_NEW here to enforce a new transaction; the
// RemoteJobDAO.newEvent() has REQUIRED.
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void addEvent(RemoteJob job, RemoteJobEvent event) {
remoteJobDAO.newEvent(job, event);
}
}
class Helper {
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public boolean addEventIfNotThere(RemoteJob job) {
// This loads the job into the persistence context associated with a new transaction.
job = remoteJobDAO.findById(job.getId());
// Locking the job record – this method is using as a semaphore by 2 threads,
// we need to make sure only one of them completes it.
remoteJobDAO.lockJob(job, LockModeType.WRITE);
// Refreshing after locking to be certain that we have current data.
remoteJobDAO.refresh(job);
// ... here comes logic for checking if EVENT_CODE_11 is not already there
if (/* not yet present */) {
remoteJobDAO.newEvent(job, EVENT_CODE_11);
}
return ...; // true - event 11 was there, false - this execution added it.
}
}
总结:在 mainMethod()
中,我们已经处于事务上下文中。然后,我们将其挂起以生成一个新事务,以在方法 addEvent()
中创建 EVENT_CODE_10。此方法返回后,我们应该提交其结果并对每个人可见(但需要刷新 mainMethod()
的上下文)。最后,我们进入 addEventIfNotThere()
方法(又是一个新事务),结果发现没有人添加 EVENT_CODE_11,所以我们执行并返回。因此,数据库中应有两个事件。
麻烦在于:OpenJPA 似乎在 addEventIfNotThere()
完成之后不会早于刷新两者事件添加事务!更重要的是,它以错误的顺序执行,并且版本列值清楚地表明第二个事务没有前一个事务的结果信息,即使第一个事务应该已提交(注意日志顺序、lastModified 字段值)和事件代码):
2011-07-08T10:45:51.386 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 1753966731> executing prepstmnt 1859546838 INSERT INTO RemoteJobEvent (id, eventCode, lastModified, version, remotejobid) VALUES (?, ?, ?, ?, ?) [params=(long) 252, (short) 11, (Timestamp) 2011-07-08 10:45:51.381, (int) 1, (long) 111]
2011-07-08T10:45:51.390 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 1753966731> executing prepstmnt 60425114 UPDATE RemoteJob SET lastModified = ?, version = ? WHERE id = ? AND version = ? [params=(Timestamp) 2011-07-08 10:45:51.381, (int) 3, (long) 111, (int) 2]
2011-07-08T10:45:51.401 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 815411354> executing prepstmnt 923940626 INSERT INTO RemoteJobEvent (id, eventCode, lastModified, version, remotejobid) VALUES (?, ?, ?, ?, ?) [params=(long) 253, (short) 10, (Timestamp) 2011-07-08 10:45:51.35, (int) 1, (long) 111]
2011-07-08T10:45:51.403 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 815411354> executing prepstmnt 1215645813 UPDATE RemoteJob SET lastModified = ?, version = ? WHERE id = ? AND version = ? [params=(Timestamp) 2011-07-08 10:45:51.35, (int) 3, (long) 111, (int) 2]
当然,这会产生 OptimisticLockException —— 它在两种环境中的行为方式相同:使用 Apache Derby/Tomcat/Atomikos Transaction Essentials 进行测试,并使用 WebSphere 7.0/Oracle 进行目标11.
我的问题是:交易边界不受尊重怎么可能?据我了解,JPA 提供程序可以在一个事务内自由选择 SQL 排序,但它不能重新排序整个事务,不是吗?
有关我们环境的更多信息:所提供的代码是 Spring 3.0.5 JMS 消息处理程序 (DefaultMessageListenerContainer) 的一部分; Spring 也用于 bean 注入,但是基于注释的事务管理使用系统事务管理器(Websphere 的/Atomikos,如上所述),这就是使用 EJB3 而不是 Spring 事务注释的原因。
我希望这能引起一些兴趣,在这种情况下,如果需要,我很乐意提供更多信息。
I have a problem with two separate transactions that are flushed to the database in a reverse order to that in which they're actually executed.
Here's the business case: there's a RemoteJob-RemoteJobEvent one-to-many relation. Every time a new event is created, a timestamp is obtained and set in the lastModified field of both RemoteJob and RemoteJobEvent, and two records are persisted (one update + one insert).
Here's what it looks like in the code:
class Main {
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void mainMethod(...) {
RemoteJob job = remoteJobDAO.findById(...);
// ...
addEvent(job, EVENT_CODE_10);
// Here the separate transaction should have ended and its results
// permanently visible in the database. We refresh the job then
// to update it with the added event:
remoteJobDAO.refresh(job); // calls EntityManager.refresh()
// ...
boolean result = helper.addEventIfNotThere(job);
}
// Annotation REQUIRES_NEW here to enforce a new transaction; the
// RemoteJobDAO.newEvent() has REQUIRED.
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void addEvent(RemoteJob job, RemoteJobEvent event) {
remoteJobDAO.newEvent(job, event);
}
}
class Helper {
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public boolean addEventIfNotThere(RemoteJob job) {
// This loads the job into the persistence context associated with a new transaction.
job = remoteJobDAO.findById(job.getId());
// Locking the job record – this method is using as a semaphore by 2 threads,
// we need to make sure only one of them completes it.
remoteJobDAO.lockJob(job, LockModeType.WRITE);
// Refreshing after locking to be certain that we have current data.
remoteJobDAO.refresh(job);
// ... here comes logic for checking if EVENT_CODE_11 is not already there
if (/* not yet present */) {
remoteJobDAO.newEvent(job, EVENT_CODE_11);
}
return ...; // true - event 11 was there, false - this execution added it.
}
}
To sum up: in mainMethod()
we are already in a transaction context. We then hang it to spawn a new transaction to create EVENT_CODE_10 in the method addEvent()
. After this method returns, we should have its results committed and visible for everyone (but the context of mainMethod()
needs to be refreshed). Finally, we step into the addEventIfNotThere()
method (a new transaction again), it turns out nobody added the EVENT_CODE_11, so we do it and return. As a result, two events should be in the database.
Here's the trouble: OpenJPA seems to flush both event-adding transactions no sooner than after the addEventIfNotThere()
completes! What's more, it does it in a wrong order, and version column values clearly show that the second transaction has no information of the results of the preceding one, even though the first one should have been committed (note the log order, lastModified field values and event codes):
2011-07-08T10:45:51.386 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 1753966731> executing prepstmnt 1859546838 INSERT INTO RemoteJobEvent (id, eventCode, lastModified, version, remotejobid) VALUES (?, ?, ?, ?, ?) [params=(long) 252, (short) 11, (Timestamp) 2011-07-08 10:45:51.381, (int) 1, (long) 111]
2011-07-08T10:45:51.390 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 1753966731> executing prepstmnt 60425114 UPDATE RemoteJob SET lastModified = ?, version = ? WHERE id = ? AND version = ? [params=(Timestamp) 2011-07-08 10:45:51.381, (int) 3, (long) 111, (int) 2]
2011-07-08T10:45:51.401 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 815411354> executing prepstmnt 923940626 INSERT INTO RemoteJobEvent (id, eventCode, lastModified, version, remotejobid) VALUES (?, ?, ?, ?, ?) [params=(long) 253, (short) 10, (Timestamp) 2011-07-08 10:45:51.35, (int) 1, (long) 111]
2011-07-08T10:45:51.403 [WorkManager.DefaultWorkManager : 7] TRACE [openjpa.jdbc.SQL] - <t 2080472065, conn 815411354> executing prepstmnt 1215645813 UPDATE RemoteJob SET lastModified = ?, version = ? WHERE id = ? AND version = ? [params=(Timestamp) 2011-07-08 10:45:51.35, (int) 3, (long) 111, (int) 2]
This, of course, produces an OptimisticLockException
-- it acts the same way in two environments: test with Apache Derby/Tomcat/Atomikos Transaction Essentials, and target with WebSphere 7.0/Oracle 11.
My question is: how is this possible, that transaction borders are not respected? I understand that a JPA provider is free to choose SQL ordering within one transaction, but it cannot reorder whole transactions, can it?
Some more info about our environment: the presented code is a part of a Spring 3.0.5 JMS message handler (DefaultMessageListenerContainer); Spring is also used for bean injections, but the annotation-based transaction management uses the system transaction manager (Websphere's/Atomikos, as above), that's why EJB3 and not Spring transactional annotations are used.
I hope this raises some interest, in which case I'll gladly supply more info, if needed.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我因为没有阅读 Spring 代理如何工作(负责基于注释的事务支持)而成为受害者。
事实证明,当从同一个类中调用该方法时,
addEvent
的 REQUIRES_NEW 注释将被忽略。 Spring 事务代理在这种情况下不起作用,因此代码在当前事务中运行 - 这是完全错误的,因为它在对helper.addEventIfNotThere()
的调用完成后(很长时间)结束。另一方面,后一种方法是从另一个类调用的,因此 REQUIRES_NEW 确实作为单独的事务启动和提交。我将
addEvent()
方法移动到一个单独的类中,问题就消失了。另一个解决方案可能是改变
配置的工作方式;更多信息在这里: Spring事务管理参考。I fell victim to not having read up on how Spring proxies work, the ones responsible for annotation-based transaction support.
It turns out the
addEvent
's REQUIRES_NEW annotation is ignored when the method is called from within the same class. The Spring transactional proxy does not come to work in this case, so the code runs in the current transaction — which is totally wrong, as it ends (long) after the call tohelper.addEventIfNotThere()
completes. The latter method, on the other hand, is called from another class, so the REQUIRES_NEW really starts and commits as a separate transaction.I moved the
addEvent()
method to a separate class and the problem disappeared. Another solution could be changing the way the<tx:annotation-driven/>
configuration works; more info here: Spring Transaction Management reference.另一种选择是使用 AspectJ 来编织 Spring 的 AnnotationTransactionAspect,如 Spring 文档
Another option would be to weave Spring's AnnotationTransactionAspect using AspectJ as described in section 11.5.9 of the Spring documentation