我在工作中遇到了一个问题,几个月来一直试图解决它,这让我疯狂。
事情很难解释,它涉及到我不允许讨论的域名的某些特殊性,我无法复制粘贴确切的代码。我将尝试通过一些有代表性的例子尽可能地清楚自己。
简单来说,系统包含一个根实体,我们称之为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;
}
答案 0 :(得分:1)
主要问题是并发性。 您的代码现在的样子,它试图检查实体是否已获得授权,何时应检查它是否已获得授权或正在获得授权。
这导致了一个重要的问题: 如何检查实体是否已在整个系统中被操纵?
我遇到过一些看似相似的情况,包括代码在集群中运行的情况。我找到的最佳工作解决方案是使用某种形式o数据库锁。
@Version应该是一个很好的快速解决方案,但你说它不能正常工作。您还声明可以使用工具审核数据库,在这种情况下检查版本字段的行为会很有趣。
没有@Version,我会尝试一些&#34; hardcore&#34;悲观的数据库锁。建议的解决方案当然不是唯一的,也不是最好的解决方案。
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语句之后。
在您的开发机器中打开两个不同的浏览器,并执行触发此代码的用例。您也可以要求团队成员从他的机器上执行此操作。
您应该看到您的IDE显示断点处执行的代码两次。在那之后,让两个执行一个接一个地执行,并享受节目。该方案应该重现。
基本上这会模拟两个同时发出的请求。
考虑: