Spring Optimistic Locking: как повторить транзакционный метод, пока фиксация не будет успешной

Я использую реализацию Spring 2.5 и Hibernate JPA с Java и "контейнерными" управляемыми транзакциями.

У меня есть метод «после фиксации пользователем», который обновляет данные в фоновом режиме и должен быть зафиксирован независимо от исключения ConcurrencyFailureException или StaleObjectStateException, потому что он никогда не будет показан клиенту. Другими словами, нужно сделать оптимистическую блокировку пессимистичной. (Может произойти, если выполнение методов займет немного больше времени и кто-то изменил данные в другой транзакции)


Я много читал об идемпотентных вещах, повторите попытку, если исключение в ищите DEFAULT_MAX_RETRIES или 6.2.7. Пример или глава 14.5. Повторите попытку. Я также нашел в stackoverflow здесь и здесь.

Я пробовал это:

public aspect RetryOnConcurrencyExceptionAspect {

    private static final int DEFAULT_MAX_RETRIES = 20;
    private int maxRetries = DEFAULT_MAX_RETRIES;

    Object around(): execution( * * (..) ) && @annotation(RetryOnConcurrencyException) && @annotation(Transactional) {

        int numAttempts = 0;
          RuntimeException failureException = null;
          do {
                numAttempts++;
                try {
                    return proceed(); 
                } 
                catch( OptimisticLockingFailureException ex ) {
                    failureException = ex;
                }
                catch(ConcurrencyFailureException ex) {
                    failureException = ex;
                }
                catch( StaleObjectStateException ex) {
                    failureException = ex;
                }
          } while( numAttempts <= this.maxRetries );
          throw failureException;

    }
}

RetryOnConcurrencyException — это моя аннотация для обозначения методов, которые необходимо повторить, если возникает исключение. Не сработало... Я также пробовал несколько способов, таких как SELECT ... FOR UPDATE, EntityManager.lock(...)

Каков наилучший способ избежать устаревших данных, грязного чтения и т. д. такой стратегии с Spring? Повторить?, синхронизировать?, блокировку JPA?, изоляцию?, выбрать... для обновления? Я не мог заставить его работать, и я очень рад любой помощи.


Вот некоторый псевдокод, который мне нравится делать:

void doSomething(itemId) {
    select something into A;
    select anotherthing into B;

    // XXX
    item = getItemFormDB( itemId ); // takes long for one user and for other concurrent user it could take less time
    item.setA(A);
    item.setB(B);

    // YYYY
    update item; 
}

Между // XXX и // YYY другая сессия может изменить элемент, тогда возникает исключение StaleObjectStateException.


person knarf1983    schedule 29.06.2010    source источник
comment
2 из ваших ссылок предназначены для Spring.net   -  person skaffman    schedule 29.06.2010
comment
Я знаю, но они решили эту проблему таким же образом...   -  person knarf1983    schedule 30.06.2010
comment
Я должен сказать, что аспект, который, по моему мнению, выполняется слишком рано, фиксация транзакции происходит позже, поэтому повторная попытка невозможна. Этой ночью я также попытался выбрать ... для обновления, блокировка сработала, но оба клиента получили исключение оптимистической блокировки (или устаревшие данные).   -  person knarf1983    schedule 30.06.2010
comment
Я нашел стиль alfresco: google.com/codesearch/p?hl=de#xjl_JOiZ61E/repos/ Есть что-то вроде этого для стандарта?   -  person knarf1983    schedule 30.06.2010


Ответы (4)


У меня есть решение, но я думаю, что оно уродливое. Я ловлю все RuntimeException, и это работает только для новых транзакций. Вы знаете, как сделать его лучше? Вы видите проблемы?

Сначала я сделал аннотацию:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryingTransaction {
     int repeatCount() default 20;
}

Затем я сделал такой перехватчик:

    public class RetryingTransactionInterceptor implements Ordered {
      private static final int DEFAULT_MAX_RETRIES = 20;
      private int maxRetries = DEFAULT_MAX_RETRIES;
      private int order = 1;

      @Resource
      private PlatformTransactionManager transactionManager;

      public void setMaxRetries(int maxRetries) {
          this.maxRetries = maxRetries;
      }
      public int getOrder() {
          return this.order;
      }
      public void setOrder(int order) {
          this.order = order;
      }

      public Object retryOperation(ProceedingJoinPoint pjp) throws Throwable {
          int numAttempts = 0;
          Exception failureException = null;
          do {
                numAttempts++;
                try {
                    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
                    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
                    TransactionStatus status = transactionManager.getTransaction(def);

                    Object obj = pjp.proceed();

                    transactionManager.commit(status);      

                    return obj;
                } 
                catch( RuntimeException re ) {
                    failureException = re;
                }
          } while( numAttempts <= this.maxRetries );
          throw failureException;
      }
}

Весеннее приложениеConfig.xml:

<tx:annotation-driven transaction-manager="transactionManager" order="10" />

<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    <property name="transactionSynchronizationName">
        <value>SYNCHRONIZATION_ALWAYS</value>
    </property>
</bean>

<bean id="retryingTransactionInterceptor" class="com.x.y.z.transaction.RetryingTransactionInterceptor">
    <property name="order" value="1" />
</bean>

<aop:config>
    <aop:aspect id="retryingTransactionAspect" ref="retryingTransactionInterceptor">
        <aop:pointcut 
            id="servicesWithRetryingTransactionAnnotation" 
            expression="execution( * com.x.y.z.service..*.*(..) ) and @annotation(com.x.y.z.annotation.RetryingTransaction)"/>
        <aop:around method="retryOperation" pointcut-ref="servicesWithRetryingTransactionAnnotation"/>
    </aop:aspect>
</aop:config>

И метод, аннотированный следующим образом:

@RetryingTransaction
public Entity doSomethingInBackground(params)...
person knarf1983    schedule 02.07.2010
comment
knarf1983, я думаю, что ваше решение отличное, и я бы хотел, чтобы оно было добавлено в Hibernate/Spring. Однако у меня есть вопрос: не должны ли мы очистить сеанс гибернации, чтобы быть уверенными, что новая транзакция загрузит новые значения из базы данных? Также я думаю, что перехват RuntimeException слишком общий для повторной попытки! Почему бы вам не придерживаться только org.hibernate.StaleObjectStateException? - person Douglas Mendes; 13.03.2013

Используйте Spring Retry, чтобы повторить весь метод, если проверка номера версии или временной метки не удалась (происходит оптимистическая блокировка ).

Конфигурация

@Configuration
@EnableRetry
public class FooConfig {
     ...
}

использование

@Retryable(StaleStateException.class)
@Transactional
public void doSomethingWithFoo(Long fooId){
    // read your entity again before changes!
    Foo foo = fooRepository.findOne(fooId);

    foo.setStatus(REJECTED)  // <- sample foo modification

} // commit on method end

Конфигурация проекта

Приложение Spring Boot определило действительную версию spring-retry, поэтому требуется только это:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency> 
person MariuszS    schedule 04.08.2017

У нас есть это, и что мы делаем, это:

  1. Сбросьте сеанс (чтобы убедиться, что предстоящее обновление будет единственным в очереди)
  2. Загрузите экземпляр
  3. Внесите изменения
  4. В StaleObjectStateException очистить очередь действий

    ((EventSource) session).getActionQueue().clear()
    

    и повторите попытку с # 2

У нас есть счетчик повторных попыток, чтобы повторно выдать исключение в конце.

ПРИМЕЧАНИЕ. Это официально не поддерживаемый метод (в Hibernate четко указано, что сеанс, вызвавший исключение, следует отбрасывать и не использовать повторно), но это известный обходной путь (с тем ограничением, что вы не можете выборочно удалить действие обновления, но должны очистить всю очередь).

person Mirvnillith    schedule 23.08.2011

Здесь отбрасываем еще один вариант: BoneCP (http://jolbox.com) поддерживает автоматический повтор транзакций в случае сбоя (включая когда БД выходит из строя, сеть выходит из строя и т. д.).

person wwadge    schedule 07.07.2010
comment
Должно. Он просто воспроизводит все, что касается дескриптора соединения/оператора. - person wwadge; 08.07.2010
comment
Но запрос на обновление будет содержать устаревшее значение оптимистической блокировки и снова завершится ошибкой. - person David Harkness; 21.03.2012