Spring乐观锁定:如何重试事务方法直到提交成功
我使用Spring 2.5和Hibernate JPA实现Java和“容器”管理事务。
我有一个“后用户提交”方法,它在后台更新数据,无论ConcurrencyFailureException
还是StaleObjectStateException
exception都需要提交,因为它永远不会显示给客户端。 换句话说,需要将乐观锁定变为悲观。 (如果方法执行需要更长的时间并且有人在其他事务中更改了数据,则可能发生)
我读了很多关于幂等的东西,如果exception搜索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
是我的Annotation,用于标记需要重试的方法(如果发生exception)。 没工作……我也尝试了几种方式,如SELECT ... FOR UPDATE
, EntityManager.lock(...)
什么是避免陈旧数据,脏读等等的最佳方法? 重试?,同步?,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。
我得到了一个解决方案,但我认为这很难看。 我捕获所有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 applicationConfig.xml:
SYNCHRONIZATION_ALWAYS
并且注释方法如下:
@RetryingTransaction public Entity doSomethingInBackground(params)...
当版本号或时间戳检查失败时(发生乐观锁定),使用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版本,因此只需要这样:
org.springframework.retry spring-retry
我们有这个,我们做的是:
- 刷新会话(以确保即将进行的更新将是排队的唯一更新)
- 加载实例
- 做改变
-
在StaleObjectStateException上,清除操作队列
((EventSource) session).getActionQueue().clear()
并从#2重试
我们有一个重试计数器,最后重新抛出exception。
注意:这不是一个官方支持的方法(Hibernate明确指出抛出exception的会话应该被丢弃而不能重复使用),但这是一个已知的解决方法(有一个限制,你不能有选择地删除更新操作,但必须清除整个队列)。
在这里抛出另一个选项:BoneCP( http://jolbox.com )支持在失败时自动重试事务(包括数据库出现故障,网络出现故障等)。