交易组合无法解释的乐观锁失败

时间:2018-10-03 09:18:43

标签: hibernate jpa optimistic-locking

摘要:我有一个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的持久层通过在一级缓存中缓存实体,直到在事务结束时刷新会话为止,表现良好。

我的代码从不手动刷新会话。

我还调查了数据损坏的可能性,例如重复引用了同一张发票,但是在我们的代码库中却没有发生(通过较早的检查也可以避免这种情况)

问题是:是什么导致了此错误,我应该如何进一步调查?

0 个答案:

没有答案