Необъяснимый сбой оптимистической блокировки при составлении транзакции - PullRequest
0 голосов
/ 03 октября 2018

Резюме: у меня есть Гейзенбаг Я не могу решить, связанные с оптимистической блокировкой Hibernate.Это происходит только тогда, когда я развертываю свое приложение в UAT и на производстве, но никогда в локальной среде.И только для кода, который выполняет два обновления для одной и той же сущности, как описано ниже

Мое приложение разработано следующим образом:

  • Родительская сущность «платеж», представляющая один атомарный платеж длянесколько бенефициаров
  • Дочерняя сущность "движение", которая является много-к-одному с Платежом и представляет собой единый платеж бенефициару.Это может быть зарплата или оплата по счету
  • Объект счета, который не является внешним ключом к движениям

Оплата счетов означает выбор счетов по их идентификатору, создание нового 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() содержит идентификатор счета.

Когда мне нужнозавершив платеж после того, как банк очистил транзакцию, я отмечу все счета, соответствующие движениям

Псевдокод:

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 имеют одинаковую структуру и извлекают объект из Hibernate Session по идентификатору, а затем выполняют обновление в конце

Поскольку я ввел расширение AbstractAuditable (вместо того, чтобы иметь четыре столбца)разбросанный по всем классам и без @Version аннотации) он начал выдавать ошибку only в 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 работает хорошо, кэшируя сущность в кэше первого уровня, пока сеанс не будет сброшен в конце транзакции.

Мой код никогда не сбрасывает сеанс вручную.

Я также исследовал возможность того, что данные были повреждены, например, с дублирующимися ссылками на тот же счет, но этого не произошло в нашей кодовой базе (это также предотвращается при более ранней проверке)

Вопрос : что является причиной этой ошибки и как мне продолжить расследование?

...