Serie de codificación habitual: recuerde un pensamiento sobre el punto muerto y la solución


dirección de github

Hay un código anotado en la dirección de github , descárguelo para una fácil lectura.

Descripción del problema

Cuando estaba escribiendo un proyecto recientemente, encontré un error. La situación es así. Tenemos un sistema de flujo de datos que informará continuamente las placas identificadas. La información de la placa informada debe procesarse de la siguiente manera

  1. Los registros de matrículas registrados se guardan en la tabla de archivos del vehículo.
  2. Llame al sistema de casos de terceros, si desea generar un nuevo caso.
  3. Si se genera un nuevo caso, se actualiza el número de casos.

La siguiente es probablemente la estructura de esta tabla

TruckRecord Long id //truckNum 已加索引 String truckNum Long caseCount

Entonces, el proceso anterior incluye tres operaciones de base de datos,

  1. seleccione 操作, seleccione truck_num de truck_recored donde truck_num = ?;
  2. Si el registro no existe, agregue una operación TruckRecord, inserte ...
  3. Llame al sistema de casos de terceros, si es un caso nuevo, actualice el número de casos nuevos.

Para no introducir otros problemas, primero determinamos tentativamente que el sistema de casos de terceros es un sistema perfecto, y me devolverá perfectamente la respuesta correcta en todo momento.

La causa del estancamiento

Por motivos comerciales, el flujo de datos envía simultáneamente dos datos con el mismo número de matrícula, por lo que los dos subprocesos han realizado operaciones de guardado y actualización respectivamente.
Porque el nivel de aislamiento utilizado por mysql es de lectura repetible.
Por lo tanto, se generará un bloqueo de intención de inserción al guardar
y se requiere la actualización para actualizar un solo registro, debido a que el registro actual no se envía, se generará un bloqueo de espacio.

El bloqueo de intención de inserción y el bloqueo de espacio ordinario se excluyen mutuamente.

Inserte la descripción de la imagen aquí

Solución de interbloqueo

Debido a que es una operación de una sola tabla, y mysql ha abierto la detección automática de la configuración de interbloqueo, o la configuración de la configuración de tiempo de espera de bloqueo, hay registros correspondientes para ver.
Para resolver la solución anterior, la operación de guardar se puede convertir en una nueva transacción por sí misma, para no causar un punto muerto.
O cambia la lógica de la siguiente manera

  1. Envíelo al sistema de terceros para devolver el resultado.
  2. Guarde o actualice el registro (la única diferencia es que cuando se encuentra que el registro está actualizado, se inserta si no existe, pero la información del caso que se acaba de devolver ya está en la información insertada)

Esto evita problemas causados ​​por la existencia simultánea de bloqueos de espacio y bloqueos de intención de inserción.
Pero la lógica anterior está realmente bien, la situación obviamente no es

Más allá del punto muerto

Debido a la existencia de mvcc, podemos seleccionar un registro y no podemos encontrar el registro no confirmado, por lo que ambos registros se actualizarán.
Para resolver este problema. Hay varias opciones.

  1. Ajuste el nivel de aislamiento de MySQL para que se convierta en el nivel no comprometido de lectura, de modo que pueda juzgar la situación que viene primero, pero una vez que la transacción se revierte, puede dar como resultado que ambos registros fallen al mismo tiempo, y ajustar el nivel de aislamiento de MySQL afecta a todo el mundo Obviamente, la operación no es una buena idea.
  2. Cree un índice único para que, incluso debido a la existencia de mvcc, se inserten simultáneamente dos registros idénticos. El mecanismo del índice único también provocará la excepción de claves duplicadas, pero el índice único no es un rendimiento particularmente bueno porque convierte la operación de inserción en una operación de un solo subproceso. No lo analizaré por separado aquí, la lógica general está relacionada con el búfer de cambio de mysql.
  3. Porque cuando un truckNum establece un bloqueo distribuido, el rango del bloqueo se controla después de la primera consulta. La lógica del código aproximado es la siguiente
public void saveOrUpdate(TruckRecord record){
    
    
	TruckRecord persist = selectByTruckNum(truckNum);
	if(persist==null){
    
    
			tryLock(record.getTruckNum());
			//这里要用new transaction 去查避免mvcc的可重复读属性
			persist=selectByTruckNum(truckNum);
			//双重检测
			if(persist==null){
    
    
				//new transaction 事务传播方式
			    saveRecord(record)
			}else{
    
    
				updateRecord(record);
			}
			unLock(record.getTruckNum());
    }

}
@Transactional(propagation = Propagation.REQUIRES_NEW)
TruckRecord selectByTruckNum(truckNum){
    
    
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
void saveRecord(truckNum){
    
    
}



De esta manera, no entrará en el estado de bloqueo después de la inserción, y la única forma de mantener el registro es maximizar el control del rango de bloqueo.
Cuando piensas ingenuamente que el código anterior no tiene hoyos, los hoyos te encontrarán naturalmente.

Error de transacción declarativa

En el código anterior, utilicé dos transacciones nuevas. En la primera consulta, me aseguré de que la consulta fuera el registro más reciente, no la vista anterior causada por una lectura constante. El segundo guardado utiliza una nueva transacción para garantizar que los datos se puedan confirmar durante el período de bloqueo. Para que la primera consulta se pueda realizar a tiempo.

Sin embargo, bajo el marco de arranque de primavera, las dos anotaciones no son efectivas. La esencia de la transacción declarativa es usar un proxy dinámico para permitir que la llamada al método original ingrese a la llamada al método proxy y ejecute una serie de códigos de aspecto. El método anterior solo puede ingresar a este método, por lo que no se genera una nueva transacción. En el caso de concurrencia, se seguirán generando dos registros idénticos debido a la lectura constante.

Manera correcta:

public void saveOrUpdate(TruckRecord record){
    
    
	TruckRecord persist = selectByTruckNum(truckNum);
	
	if(persist==null){
    
    
			tryLock(record.getTruckNum());
			TruckService proxy = SpringUtils.getBean(TruckService.class);
			//这里要用new transaction 去查避免mvcc的可重复读属性
			persist=proxy.selectByTruckNum(truckNum);
			//双重检测
			if(persist==null){
    
    
				//new transaction 事务传播方式
			    proxy.saveRecord(record)
			}else{
    
    
				updateRecord(record);
			}
			unLock(record.getTruckNum());
    }

}

Por supuesto, debido a la implementación de bloqueos distribuidos en bloqueos distribuidos, como los bloqueos distribuidos redis, en el caso de fallas de bloqueo , aún se pueden generar dos registros en casos muy extremos. Se recomienda que para escenarios donde los datos son muy precisos y la operación de inserción no es tan frecuente, usar un candado único sigue siendo una buena solución.

para resumir

Este artículo es principalmente para reproducir una pequeña escena en el proyecto y luego pensar en la situación. Por supuesto, también debe considerar la situación idempotente del sistema de terceros, la situación de reintento de error y la falla de bloqueo. Soluciones correctivas, etc., en realidad no existe una respuesta única para estas en diferentes sistemas, por lo que aún se necesita un análisis caso por caso.

Supongo que te gusta

Origin blog.csdn.net/qq_33361976/article/details/109891654
Recomendado
Clasificación