tutorial net mvc framework espaƱol curso capas asp .net entity-framework transactions race-condition

.net - net - mvc n capas



Prevenir la condiciĆ³n de carrera de if-exists-update-else-insert en Entity Framework (3)

He estado leyendo otras preguntas sobre cómo implementar la semántica if-exists-insert-else-update en EF, pero o no estoy entendiendo cómo funcionan las respuestas, o de hecho no están abordando el problema. Una solución común que se ofrece es ajustar el trabajo en un ámbito de transacción (p. Ej .: Implementar if-not-exists-insert usando Entity Framework sin condiciones de carrera ):

using (var scope = new TransactionScope()) // default isolation level is serializable using(var context = new MyEntities()) { var user = context.Users.SingleOrDefault(u => u.Id == userId); // * if (user != null) { // update the user user.property = newProperty; context.SaveChanges(); } else { user = new User { // etc }; context.Users.AddObject(user); context.SaveChanges(); } }

Pero no veo cómo esto resuelve nada, para que esto funcione, la línea que he marcado arriba debería bloquearse si un segundo hilo intenta acceder al mismo ID de usuario, desbloqueándose solo cuando el primer hilo ha terminado su trabajo. Sin embargo, el uso de una transacción no causará esto, y obtendremos una UpdateException debido a la violación de la clave que ocurre cuando el segundo subproceso intenta crear el mismo usuario por segunda vez.

En lugar de atrapar la excepción causada por la condición de carrera, sería mejor evitar que la condición de carrera ocurra en primer lugar. Una forma de hacerlo sería que la línea marcada con estrellas establezca un bloqueo exclusivo en la fila de la base de datos que coincida con su condición, lo que significa que en el contexto de este bloque, solo un hilo a la vez podría funcionar con un usuario.

Parece que este debe ser un problema común para los usuarios de EF, por lo que estoy buscando una solución limpia y genérica que pueda usar en cualquier lugar.

Realmente me gustaría evitar el uso de un procedimiento almacenado para crear mi usuario si es posible.

¿Algunas ideas?

EDITAR : Intenté ejecutar el código anterior al mismo tiempo en dos subprocesos diferentes utilizando la misma ID de usuario, y a pesar de sacar transacciones serializables, ambos pudieron ingresar a la sección crítica (*) al mismo tiempo. Esto dio lugar a que se lanzara una UpdateException cuando el segundo subproceso intentó insertar el mismo ID de usuario que el primero acababa de insertar. Esto se debe a que, como señala Ladislav a continuación, una transacción serializable toma bloqueos exclusivos solo después de que haya comenzado a modificar los datos, no a leer.


Tal vez me falta algo, pero cuando simulo el ejemplo anterior en el SQL Management Studio, esto está funcionando como se esperaba.

Ambas transacciones Serializable comprueban si el ID de usuario existe y adquieren bloqueos de rango en la selección especificada.

Suponiendo que este ID de usuario no existe, ambas transacciones intentan insertar un nuevo registro con el ID de usuario, lo que no es posible. Debido a su nivel de aislamiento serializable, ambas transacciones no pueden insertar un nuevo registro en la tabla de usuarios porque esto introduciría lecturas fantasmas para la otra transacción.

Entonces, esta situación resulta en un punto muerto debido a los bloqueos de rango. Terminará con un punto muerto y una transacción será victimizada, la otra tendrá éxito.

¿Entity Framework es diferente? Sospecho que terminarás con una UpdateException con una SqlException anidada que identifica el interbloqueo.


Al usar transacciones serializables, SQL Server emite bloqueos compartidos en registros / tablas de lectura. Los bloqueos compartidos no permiten que otras transacciones modifiquen los datos bloqueados (las transacciones se bloquearán), pero permiten que otras transacciones lean datos antes de que la transacción que emitió los bloqueos comience a modificar los datos. Esa es la razón por la cual el ejemplo no funciona: se permiten lecturas concurrentes con bloqueos compartidos hasta que la primera transacción comience a modificar los datos.

Desea aislamiento, donde el comando de selección bloquea la tabla completa exclusivamente para un solo cliente. Debe bloquear toda la tabla porque de lo contrario no resolverá la concurrencia para insertar el "mismo" registro. El control granular para bloquear registros o tablas mediante comandos de selección es posible cuando se utilizan sugerencias, pero debe escribir consultas SQL directas para usarlas; EF no tiene soporte para eso. Describí el enfoque para bloquear exclusivamente esa tabla aquí, pero es como crear acceso secuencial a la tabla y afecta a todos los demás clientes que acceden a esta tabla.

Si está realmente seguro de que esta operación ocurre solo en su único método y no hay otras aplicaciones que utilicen su base de datos, simplemente puede colocar el código en la sección crítica (sincronización .NET por ejemplo con lock ) y garantizar en el lado .NET que solo un solo hilo puede acceder a la sección crítica. Esa no es una solución tan confiable, pero cualquier juego con bloqueos y niveles de transacción tiene un gran impacto en el rendimiento y rendimiento de la base de datos. Puede combinar este enfoque con concurrencia optimista (restricciones únicas, marcas de tiempo, etc.).


Puede cambiar el nivel de aislamiento de transacción utilizando TransactionOptions for TransactionScope a más estricto (supongo que para su caso es RepeatableRead o Serializable), pero recuerde que cualquier bloqueo reduce la escalabilidad.

¿Realmente importa proporcionar tal nivel de control de concurrencia? ¿Se usará su aplicación en los mismos casos en el entorno de producción? Aquí hay una buena publicación de Udi Dahan sobre las condiciones de carrera.