Spring乐观锁:如何重试事务方法直到提交成功

22
我使用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 UPDATEEntityManager.lock(...)等方法。

如何在Spring中避免脏读取等问题?采用哪种策略最好?重试?同步?JPA锁?隔离级别?使用 select ... for update?我无法让它生效,非常感谢任何帮助。


下面是我想做的一些伪代码:

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 异常。


你的两个链接是关于Spring.net的。 - skaffman
我知道,但他们用同样的方法解决了这个问题... - knarf1983
我需要说的是,我认为的这个方面被“过早”执行,事务的提交发生在稍后,因此重试是不可能的。今晚我还尝试了select ... for update,锁定起作用,但两个客户端都遇到了乐观锁异常(或陈旧数据异常)。 - knarf1983
4个回答

10

使用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> 

8

我有一个解决方案,但我认为它很丑陋。我捕获所有的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;
      }
}

Spring应用程序配置文件:

<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)...

knarf1983,我认为你的解决方案很棒,希望它可以被添加到Hibernate/Spring中。但是我有一个问题:我们不必清除Hibernate会话以确保新事务从数据库中加载新值吗?此外,我认为捕获RuntimeException对于重试来说太泛泛了!为什么不坚持使用"org.hibernate.StaleObjectStateException"呢? - Douglas Mendes

2
我们有这个,我们所做的是:
  1. Flush the session (to make sure the upcoming update will be the only one queued)
  2. Load the instance
  3. Do the change
  4. On StaleObjectStateException, clear the action queue

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

    and retry from #2

我们有一个重试计数器,在最后重新抛出异常。
注意:这不是官方支持的方法(Hibernate明确表示,抛出异常的会话应该被丢弃而不是重新使用),但这是一个已知的解决方法(限制是你不能选择性地删除更新操作,而必须清除整个队列)。

-2
另外提供一个选择:BoneCP(http://jolbox.com)支持在事务失败时自动重试(包括数据库宕机、网络故障等)。

它应该这样做。它只是重放命中连接/语句句柄的任何内容。 - wwadge
5
但是更新查询将包含过时的乐观锁定值,并再次失败。 - David Harkness

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接