我在工作中遇到一个问题,尝试了几个月才解决,这让我发疯。
这件事很难解释,它涉及我不允许讨论的领域的某些特殊性,而且我无法复制粘贴确切的代码。我将通过一些代表性的例子来使自己尽可能清楚。
简而言之,系统包含一个根实体,我们称其为MainDocument实体。在这个实体周围,有几个绕行的实体。 MainDocument
实体具有状态。我们将此状态称为“MainDocumentState”。
public class MainDocument {
@OneToOne
@JoinColumn(name = "document_state_id")
MainDocumentState state;
@Version
long version = 0L;
}
大约有10个州可用,但是在此示例中,将重点介绍其中两个州。我们称它们为
ReadyForAuthorization
和Authorized
。这就是您需要知道的所有示例。
关于我们正在使用的技术:
关于问题本身:
系统的一部分至关重要,它可以处理大多数传入流量。我们将此部分称为“授权部分”。在本节中,我们通过由美国海关和边境保护局提供的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);
}
}
丢失的更新何时发生以及如何影响系统:
进行身份验证。
ID为1的MainDocument保持状态为ReadyForAuthorization,而正确的状态应为Authorized。
之所以会产生复杂性,是因为几乎无法复制。它仅在生产中发生,即使我尝试向服务器充斥数百个调用,也无法获得相同的行为。
实施的解决方案:
如果有人具有并发和事务管理经验,可以给我一些有关如何调试或重现此问题的技巧,或者至少实施一些减轻损失的解决方案,我将不胜感激。
需要说明的是,每小时有1000多个请求,其中99.99%正确结束。每月出现此问题的案例总数约为20。
添加09-13-17:
如果需要,我们使用的
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;
}
最佳答案
主要问题是并发性。
您的代码现在看起来是这样的,它试图检查实体是否被授权,何时应该检查实体是否被授权或者正在被授权。
它导致了一个重要的问题:
如何检查实体是否已在整个系统中被操纵?
我遇到过一些看起来很相似的情况,包括在集群中运行代码的场景。我发现最好的工作解决方案是使用某种形式的数据库锁。
@Version应该是一个很好的快速解决方案,但是您表示它无法正常工作。您还说过,您可以使用工具审核数据库,在这种情况下,检查版本字段的表现会很有趣。
如果没有@Version,我将尝试一些“核心”悲观数据库锁定。提出的解决方案当然不是唯一的,也不是最佳的解决方案。
1-创建一个新表。该表将存储正在处理的文档的ID。 PK应该是文档ID,或其他可以确保同一文档在此表中没有重复的内容。
2-在检索实体之前,在您的代码中检查ID是否在步骤1中创建的表中。如果不是,请继续。
如果是这样,则假定它正在处理中并且什么也不做。
3-在检索实体之后,在代码中,您必须在步骤1中创建的表中插入ID。
如果未授权文档,则插入将成功,并且过程将继续。
如果有机会同时执行两个请求,则其中一个请求将获得约束违例异常(或类似的异常)。然后,您的代码应假定该文档已被授权。
重要:必须在新事务中执行插入。用于将Id保留在新表中的spring bean应该将其方法标记为@Transaction(propagation = Propagation.REQUIRES_NEW)
。
4-调用Webservice并正确处理了响应后,从步骤1中创建的表中删除ID。它也应在单独的事务中执行。
考虑在finally块中执行此操作,因为如果发生任何其他运行时错误,则应从表中删除文档ID。
如何调试:
检索实体,并在将其插入新表之前。如果要调试当前代码,则将断点放在Assert语句之后。
注意事项: