Administrar la concurrencia cuando se usa el patrón SELECT-UPDATE

25

Digamos que tiene el siguiente código (ignore que es horrible):

BEGIN TRAN;
DECLARE @id int
SELECT @id = id + 1 FROM TableA;
UPDATE TableA SET id = @id; --TableA must have only one row, apparently!
COMMIT TRAN;
-- @id is returned to the client or used somewhere else

En mi opinión, esto NO está gestionando la concurrencia correctamente. El hecho de que tenga una transacción no significa que otra persona no leerá el mismo valor que usted antes de llegar a su estado de actualización.

Ahora, dejando el código como está (me doy cuenta de que esto se maneja mejor como una sola declaración o incluso mejor usando una columna de autoincremento / identidad), ¿cuáles son formas seguras de hacer que maneje la concurrencia correctamente y evite las condiciones de carrera que permiten que dos clientes obtengan lo mismo? valor de identificación?

Estoy bastante seguro de que agregar un WITH (UPDLOCK, HOLDLOCK)a SELECT hará el truco. El nivel de aislamiento de la transacción SERIALIZABLE parece funcionar también, ya que niega a otra persona que lea lo que hizo hasta que termine el tran ( ACTUALIZACIÓN : esto es falso. Vea la respuesta de Martin). ¿Es eso cierto? ¿Funcionarán ambos igualmente bien? ¿Se prefiere uno sobre el otro?

Imagine hacer algo más legítimo que una actualización de ID: algunos cálculos basados ​​en una lectura que necesita actualizar. Podría haber muchas tablas involucradas, algunas de las cuales escribirás y otras no. ¿Cuál es la mejor práctica aquí?

Después de escribir esta pregunta, creo que las sugerencias de bloqueo son mejores porque entonces solo está bloqueando las tablas que necesita, pero agradecería el aporte de cualquiera.

PD: ¡No, no sé la mejor respuesta y realmente quiero entenderlo mejor! :)

ErikE
fuente
Solo para aclarar: ¿desea evitar que 2 clientes lean el mismo valor o emitan datos updateque pueden estar basados ​​en datos obsoletos? Si es esto último, puede usar la rowversioncolumna para verificar si la fila a actualizar no ha cambiado desde que se leyó.
a1ex07
No queremos que un segundo cliente obtenga el valor de identificación anterior antes de que el primer cliente lo actualice al nuevo valor. Debería bloquear.
ErikE

Respuestas:

11

Solo aborda el SERIALIZABLEaspecto del nivel de aislamiento. Sí, esto funcionará pero con un riesgo de punto muerto.

Dos transacciones podrán leer la fila simultáneamente. No se bloquearán entre sí, ya que tomarán un Sbloqueo de objeto o RangeS-Sbloqueos de índice que dependen de la estructura de la tabla y estos bloqueos son compatibles . Pero se bloquearán entre sí cuando intenten adquirir los bloqueos necesarios para la actualización ( IXbloqueo de objetos o índice, RangeS-Urespectivamente), lo que conducirá a un punto muerto.

El uso de una UPDLOCKsugerencia explícita en su lugar serializará las lecturas evitando así el riesgo de punto muerto.

Martin Smith
fuente
+1 pero: para las tablas de montón todavía puede obtener un punto muerto de conversión incluso con bloqueos de actualización: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/03/11/…
AK
Extraño, @alex. Me imagino que tiene que ver con una condición de carrera del motor tratando de encontrar lo que se bloquee antes de que realmente se UPDLOCKing ...
Erike
@ErikE: el punto muerto de conversión en el artículo de Alex se está convirtiendo IXen Xen el montón mismo. Curiosamente, ninguna fila califica, por lo que nunca se eliminan los bloqueos de fila. No estoy seguro de por qué toma la Xcerradura en absoluto.
Martin Smith
11

Creo que el mejor enfoque para usted sería exponer su módulo a una alta concurrencia y verlo por usted mismo. Algunas veces UPDLOCK solo es suficiente, y no hay necesidad de HOLDLOCK. A veces sp_getapplock funciona muy bien. No haría ninguna declaración general aquí; a veces, agregar un índice, desencadenador o vista indizada más cambia el resultado. Necesitamos enfatizar el código de prueba y ver por nosotros mismos caso por caso.

He escrito varios ejemplos de pruebas de estrés aquí.

Editar: para un mejor conocimiento de los aspectos internos, puede leer los libros de Kalen Delaney. Sin embargo, los libros pueden perder la sincronización al igual que cualquier otra documentación. Además, hay demasiadas combinaciones a considerar: seis niveles de aislamiento, muchos tipos de bloqueos, índices agrupados / no agrupados y quién sabe qué más. Eso es muchas combinaciones. Además de eso, SQL Server es de código cerrado, por lo que no podemos descargar el código fuente, depurarlo y demás, esa sería la última fuente de conocimiento. Cualquier otra cosa puede estar incompleta o desactualizada después de la próxima versión o paquete de servicio.

Por lo tanto, no debe decidir qué funciona para su sistema sin su propia prueba de esfuerzo. Lo que sea que haya leído, puede ayudarlo a comprender lo que está sucediendo, pero debe probar que el consejo que ha leído funciona para usted. No creo que nadie pueda hacerlo por ti.

Alaska
fuente
9

En este caso particular, la adición de una UPDLOCKcerradura al SELECTevitaría anomalías. La adición de HOLDLOCKno es necesaria ya que se mantiene un bloqueo de actualización durante la transacción, pero confieso haberlo incluido yo mismo como un hábito (posiblemente malo) en el pasado.

Imagine hacer algo más legítimo que una actualización de ID, algunos cálculos basados ​​en una lectura que necesita actualizar. Podría haber muchas tablas involucradas, algunas de las cuales escribirás y otras no. ¿Cuál es la mejor práctica aquí?

No hay mejores prácticas. Su elección del control de concurrencia debe basarse en los requisitos de la aplicación. Algunas aplicaciones / transacciones deben ejecutarse como si tuvieran la propiedad exclusiva de la base de datos, evitando a toda costa anomalías e inexactitudes. Otras aplicaciones / transacciones pueden tolerar cierto grado de interferencia entre sí.

  • Recuperar un nivel de stock en bandas (<5, 10+, 50+, 100+) para un producto en una tienda web = lectura sucia (imprecisa no importa).
  • Comprobación y reducción del nivel de existencias en el pago de la tienda web = lectura repetible (DEBEMOS tener las existencias antes de vender, NO DEBEMOS terminar con un nivel de existencias negativo).
  • Mover efectivo entre mi cuenta corriente y de ahorro en el banco = serializable (¡no calcule mal ni extravíe mi efectivo!).

Editar: el comentario de @ AlexKuznetsov me llevó a releer la pregunta y eliminar el error muy obvio en mi respuesta. Nota personal sobre la publicación nocturna.

Mark Storey-Smith
fuente