java - registros - tabla de bloqueo base de datos
Bloqueo optimista de Spring: cómo reintentar el método transaccional hasta que la confirmación sea exitosa (4)
Utilizo Spring 2.5 y la implementación de Hibernate JPA con Java y las transacciones gestionadas por "contenedores".
Tengo un método "después de la confirmación del usuario" que actualiza los datos en segundo plano y StaleObjectStateException
confirmarlos independientemente de ConcurrencyFailureException
excepción ConcurrencyFailureException
o StaleObjectStateException
, porque nunca se mostrará al cliente. En otras palabras, es necesario hacer un bloqueo optimista a pesimista. (Podría suceder si la ejecución de los métodos tomara un poco más de tiempo y alguien cambiara los datos en otra transacción)
Leí mucho sobre cosas idempotentes, reintento si es una excepción en la búsqueda de DEFAULT_MAX_RETRIES o 6.2.7. Ejemplo o capítulo 14.5. Reintentar También encontré en stackoverflow here y here .
Intenté esto:
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
es mi anotación para marcar los métodos que deben reintentarse, si se produce una excepción. No funcionó ... También probé varias formas como SELECT ... FOR UPDATE
, EntityManager.lock(...)
¿Cuál es la mejor manera de evitar datos obsoletos, lecturas sucias, etc., como una estrategia con Spring? Vuelva a intentarlo, ¿sincronizado ?, ¿Bloqueo JPA ?, ¿aislamiento ?, ¿seleccione ... para actualizar? No pude hacerlo funcionar y estoy realmente feliz por cualquier ayuda.
Aquí hay un pseudo código que me gusta hacer:
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;
}
Entre // XXX y // YYY otra sesión podría modificar el elemento, luego se lanza la StaleObjectStateException.
Desechando otra opción aquí: BoneCP ( http://jolbox.com ) tiene soporte para reintentar automáticamente las transacciones en caso de fallo (incluso cuando el DB se cae, falla la red, etc.).
Tenemos esto y lo que hacemos es:
- Vacíe la sesión (para asegurarse de que la próxima actualización será la única en cola)
- Cargar la instancia
- Hacer el cambio
En StaleObjectStateException, borre la cola de acciones
((EventSource) session).getActionQueue().clear()
y vuelva a intentar desde el # 2
Tenemos un contador de reintentos para volver a lanzar la excepción al final.
NOTA: Este no es un método admitido oficialmente (Hibernate establece claramente que una sesión que ha generado una excepción debe descartarse y no volver a usarse), pero es una solución alternativa conocida (con la limitación de que no puede eliminar la Acción de actualización, pero debe borrar toda la cola).
Tengo una solución pero creo que es fea. Capturo todas las RuntimeException y solo funciona para nuevas transacciones. ¿Sabes cómo hacerlo mejor? ¿Ves algún problema?
Primero, hice una anotación:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryingTransaction {
int repeatCount() default 20;
}
Entonces hice un interceptor como este:
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:
<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>
Y un método anotado así:
@RetryingTransaction
public Entity doSomethingInBackground(params)...
Utilice Spring Reintentar para reintentar todo el método cuando haya fallado un número de versión o una comprobación de marca de tiempo (se produce un bloqueo optimista).
Configuración
@Configuration
@EnableRetry
public class FooConfig {
...
}
Uso
@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
Configuración del proyecto
La aplicación Spring Boot ha definido una versión válida de reintento de primavera, por lo que solo se requiere esto:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>