Резюме: у меня есть Гейзенбаг Я не могу решить, связанные с оптимистической блокировкой 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 работает хорошо, кэшируя сущность в кэше первого уровня, пока сеанс не будет сброшен в конце транзакции.
Мой код никогда не сбрасывает сеанс вручную.
Я также исследовал возможность того, что данные были повреждены, например, с дублирующимися ссылками на тот же счет, но этого не произошло в нашей кодовой базе (это также предотвращается при более ранней проверке)
Вопрос : что является причиной этой ошибки и как мне продолжить расследование?