摘要:我有一个Heisenbug无法解决与Hibernate乐观锁定有关的问题。仅当我将应用程序部署到UAT和生产环境时才会发生这种情况,而不会在本地环境上发生。而且仅适用于如下所述在同一实体上执行两次更新的代码
我的应用程序设计如下:
支付发票意味着按发票ID选择发票,创建一个新的Payment
实体,其中每个Movement
支付都包含一个Invoice
。
@MappedSuperclass
public class AbstractAuditable //Provides auditing and optimistic locking
{
@Column(name = "CREATED", nullable = false, updatable = false)
protected final LocalDateTime created = LocalDateTime.now();
@Column(name = "CREATOR", length = 100, nullable = false, updatable = false)
protected String creator;
@Column(name = "MODIFIED", nullable = false)
@Version
protected LocalDateTime modified = LocalDateTime.now();
@Column(name = "MODIFIER", length = 100, nullable = false)
protected String modifier;
}
@Entity
public class Payment extends AbstractAuditable
{
@Id
protected Long id;
@OneToMany(mappedBy = "payment", fetch = FetchType.EAGER, cascade = CascadeType.ALL, targetEntity = Movement.class, orphanRemoval = true)
@OrderBy("progressivo asc")
@JsonManagedReference
protected final List<Movement> movements = new LinkedList<>();
protected String debitIban;
protected BigDecimal totalAmount = BigDecimal.ZERO;
}
@Entity
public class Movement extends AbstractAuditable {
@Id
protected Long id;
protected String beneficiary, creditIban;
protected BigDecimal amount;
protected Class<?> referenceType;
protected Long referenceId;
}
工作方式:当Movement#getReferenceType() == Invoice.class
时,Movement#getReferenceId()
持有发票ID。
当我需要在银行清算交易后完成付款时,我会标记所有与移动相关的发票
伪代码:
fetch payment by id
foreach (var movement in payment)
{
if (movement's reference type is Invoice)
{
update Invoice by movement.referenceId: set a flag; update partially-paid amount; update invoice status to paid
}
}
update payment: set status to closed
以下是真实代码:
@Override
@Transactional
@Retryable(include = { ConcurrencyFailureException.class, LockAcquisitionException.class }, maxAttempts = 5, backoff = @Backoff(random = true, maxDelay = 1000))
public Payment setPaid(Long paymentId, LocalDate paymentDate, String username)
{
Payment Payment = getDao().findById(paymentId); //session.get
if (Payment == null)
throw new NotFoundException(Payment.class, paymentId);
getDao().lock(Payment); //SELECT for update
if (paymentDate == null)
throw new NullPointerException();
if (Payment.getStatus() != Status.SENT)
throw new IllegalStateException("Illegal state");
if (Payment.getType() == PaymentTipo.SUPP)
for (Movement move : Payment.getMovements())
if (move.getReferenceType() == Invoice.class && move.getReferenceId() != null)
{
Long invoiceId = move.getReferenceId();
try
{
invoiceManager.setPaymentFlag(invoiceId, false, username);
invoiceManager.updatePaidAmount(invoiceId, move.getAmount(), username);
}
catch (NotFoundException ex)
{
throw new RuntimeException(ex);
}
catch (RuntimeException ex)
{
throw new RuntimeException(ex);
}
}
Payment.setStatus(Status.DONE);
Payment.setExecutionDate(paymentDate);
Payment.modify(username); //Updates superclass modifier, does not touch @Version as it is handled by Hibernate
getDao().update(Payment); //session.update
return Payment;
}
InvoiceManager
中的所有方法都具有相同的结构,并将通过ID从休眠Session
中获取对象,然后最后执行更新
由于我引入了AbstractAuditable的扩展(而不是将四列分散在所有类中,并且没有 @Version
注释),因此开始在UAT中仅仅失败并产生相同的错误。
上面的代码适用于单个事务,因为传播是必需的,而不是REQUIRES_NEW。今天,我尝试添加SQL锁,以查看是否可以更早地捕获该错误。
与发票不相关的每笔付款(是的,当您支付薪水时,这都是可能的)就像一个护身符。与单个发票相关的某些付款也可以使用。但是几乎所有其他情况都对OptimisticLockException
无效。
例外是
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
java.lang.RuntimeException: org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at PaymentManager.setPaid
.....
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) ~[spring-tx-4.3.19.RELEASE.jar:4.3.19.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) ~[spring-tx-4.3.19.RELEASE.jar:4.3.19.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.19.RELEASE.jar:4.3.19.RELEASE]
....
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-util.jar:8.0.39]
at java.lang.Thread.run(Thread.java:745) [?:1.8.0_112]
Caused by: org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:283) ~[spring-orm-4.3.19.RELEASE.jar:4.3.19.RELEASE]
...
at InvoiceManager.setFlag
......
Caused by: org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:67) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:54) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:46) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3198) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3077) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3457) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:145) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:589) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:463) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:50) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1264) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1332) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
at org.hibernate.internal.QueryImpl.list(QueryImpl.java:87) ~[hibernate-core-5.1.16.Final.jar:5.1.16.Final]
...
at BaseDaoImpl.findById
.....
at InvoiceManager.setFlag
........
我知道调用多个事务方法不是最佳设计。应该重构代码以一次对Invoice对象进行所有修改,而不是执行单个操作,更新实体并再次获取同一实体以执行其他更新。但是,我知道Hibernate的持久层通过在一级缓存中缓存实体,直到在事务结束时刷新会话为止,表现良好。
我的代码从不手动刷新会话。
我还调查了数据损坏的可能性,例如重复引用了同一张发票,但是在我们的代码库中却没有发生(通过较早的检查也可以避免这种情况)
问题是:是什么导致了此错误,我应该如何进一步调查?