【发布时间】:2017-09-14 02:22:32
【问题描述】:
我在工作中遇到了一个问题,我花了几个月的时间来解决它,这让我发疯了。
这件事很难解释,它涉及我不允许讨论的领域的一些特殊性,而且我无法复制粘贴确切的代码。我会尽量用一些有代表性的例子来说明自己。
简单地说,系统包含一个根实体,我们称它为 MainDocument 实体。在这个实体周围,有几个实体在绕行。 MainDocument 实体有一个状态。我们称这个状态为“MainDocumentState”。
public class MainDocument {
@OneToOne
@JoinColumn(name = "document_state_id")
MainDocumentState state;
@Version
long version = 0L;
}
大约有 10 个状态可用,但在此示例中将重点介绍其中两个。让我们打电话给他们,ReadyForAuthorization 和 Authorized。
这就是你需要知道的例子。
关于我们正在使用的技术:
- 春天
- GWT Web 应用程序
- Java 1.6
- 休眠
- JPA
- Oracle 数据库。
关于问题本身:
系统中有一部分很关键,负责处理大部分传入流量。我们将此部分称为“授权部分”。在此部分,我们通过我国海关和边境保护局提供的 SOAP WS 发送信息,以授权MainDocument 针对海关。
代码如下所示:
@Transactional
public void authorize(Integer mainDocId) {
MainDocument mainDocument = mainDocumentService.findById(mainDocId);
// if document is not found, an exception is thrown.
Assert.isTrue(mainDocument.notAutorized(), "The document is already authorized");
// more bussiness logic validations happen here. This validations are not important for the topic discussed here. They make sure that the document meets some basic preconditions.
try {
Transaction aTransaction = transactionService.newTransaction(); // creates a transaction, an entity stored in the database that keeps track of all the authorization service calls
try {
Response response = wsAuthroizationService.sendAuthorization(mainDocument.getId(), mainDocument.getAuthorizationId()); // take into account that sometimes this call can take between 2-4 minutes.
catch (Exception e) {
aTransaction.failed();
transactionService.saveOrUpdate(aTransaction);
throw e;
}
// the behaviour is the same for every error code.
if (response.getCode() != 0) {
aTransaction.setErrorCode(resposne.getCode());
transactionService.saveOrUpdate(aTransaction);
throw AuthroizationError("Error on auth");
}
aTransaction.completed();
mainDocument.setAuthorizationCode(0);
mainDocument.authorize(); // will change state to "Authorized"
} catch (Exception e) {
mainDocument.authorize(); // will not change state because authorizationCode != 0 or its null.
} finally {
saveOrUpdate(mainDocument);
}
}
丢失更新何时发生以及它如何影响系统:
- MainDocument id: 1@Thread-1 尝试授权
- 文档未授权,继续执行
- 通过webservice授权OK
- 事务关闭并发生提交。
- 当 1 提交时,MainDocument 1@Thread-2 进来,并尝试 进行身份验证。
- 1 尚未持久化,Thread-2 尝试进行身份验证。
- 线程 2 被 WS 拒绝,响应为“文档 1 已被授权”。
- Thread-2 尝试提交。
- Thread-1 首先提交文档 1,Thread-2 在第二位提交。
id:1 的 MainDocument 以 ReadyForAuthorization 状态持久化,而正确的状态应该是 Authorized。
之所以会出现复杂性,是因为它几乎不可能重现。它只发生在生产环境中,即使我尝试用数百次调用淹没服务器,我也无法获得相同的行为。
实施的解决方案:
- 线程屏障,如果两个具有相同MainDocument id的线程尝试授权,最后进入的线程被拒绝。它是用一个方面实现的,顺序为 100,因此它在 @Transactional 提交之后执行。在方面拦截并从屏障中移除线程之前,对事务提交的堆栈跟踪进行了测试和检查。
- @Version,适用于系统的其他部分,当一个提交试图覆盖来自旧事务的另一个提交时,会引发 OptimisticLockException。在这种情况下,不会引发 OptimisticLockException。
- “事务”与 @Transactional(propagation = REQUIRES_NEW) 保持一致,因此它独立于主事务并且已正确提交。通过这些事务,很明显丢失更新是一个问题,因为我们可以看到带有成功消息的已完成事务,并且 MainDocument 保持不同的状态,server.log 上没有显示任何错误。
- 使用 Imperva SecureSphere,我们可以审计特定表上的所有更新。我们可以清楚地看到第一个事务以正确的状态提交,而第二个事务覆盖了第一个事务。
如果有并发和事务管理经验的人能给我一些关于如何调试或重现问题的有用提示,或者至少实施一些解决方案来减轻损失,我将不胜感激。
需要明确的是,每小时有超过 1000 个请求,其中 99.99% 的请求正确结束。每月出现此问题的案例总数约为 20 个。
于 17 年 9 月 13 日添加:
如果需要,我们正在使用的saveOrUpdate 方法:
* "http://blog.xebia.com/2009/03/23/jpa-implementation-patterns-saving-detached-entities/" >JPA
* implementation patterns: Saving (detached) entities</a>
*
* @param entity
*/
protected E saveOrUpdate(E entity) {
if (entity.getId() == null) {
getJpaTemplate().persist(entity);
return entity;
}
if (!getJpaTemplate().getEntityManager().contains(entity)) {
return merge(entity);
}
return entity;
}
【问题讨论】:
-
MainDocument 不应在方法中过去。而是传递文档 ID 并通过 ID 在方法中检索。您也可以为文档尝试悲观锁(SELECT FOR UPDATE)。要重现尝试在方法中添加 sleep(...)
-
是的,对不起,代码不具体,这是正确的标头:
public UnaDeclaracionJuradaResult informarDestinacionAduana( final InformarDestinacionAduanaParameter parametroInformarDestinacion) {那个Parameter对象有id,实体是用findById获取的。 -
由于事务率非常低,您是否尝试过调整数据库的事务隔离设置(或者只是检查它是什么,对于初学者来说)看看它是否改变了什么?如果不出意外,它消除了一个可能的问题来源。
-
@OneToOne MainDocumentState 状态; - 您确定整个表格中每个州只有一个文档吗?关联应该是多对一吗?
-
@dimirsen 实际上你是对的,它应该是“@ManyToOne”。问题是我不知道这与这个特定问题有什么关系。总之值得一试。
标签: java spring hibernate jpa concurrency