春季乐观锁定:如何在提交成功之前重试事务方法

我将Spring 2.5和Hibernate JPA实现与Java和“容器”托管事务结合使用。

我有一个“用户提交后”方法,该方法在后台更新数据,并且无论是否异常都需要提交,因为它永远不会显示给客户端。换句话说,需要让乐观锁定到悲观。(如果方法执行需要更长的时间并且有人在其他事务中更改了数据,则可能会发生这种情况)ConcurrencyFailureExceptionStaleObjectStateException


我读了很多关于幂等的东西,如果异常,请重试,以搜索DEFAULT_MAX_RETRIES6.2.7。示例第 14.5 章。重试。我也在这里这里发现了堆栈溢出。

我试过这个:

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 UPDATEEntityManager.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。


答案 1

我有一个解决方案,但我认为这很丑陋。我捕获了所有运行时异常,它仅适用于新事务。你知道如何让它变得更好吗?你看到有什么问题吗?

首先,我做了一个注释:

@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;
      }
}

弹簧应用配置.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)...

答案 2

使用 Spring 重试可在版本号或时间戳检查失败(发生乐观锁定)时重试整个方法。

配置

@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 重试版本,因此只需要以下版本:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency> 

推荐