2009-11-05 20 views
7

(Nota: esto es para el servidor MS SQL)SQL Server Condición de condición Pregunta

Supongamos que tiene una tabla ABC con una columna de identidad de clave principal y una columna CODE. Queremos que cada fila aquí tenga un código único, generado secuencialmente (basado en alguna fórmula típica de dígito de control).

Digamos que tiene otra tabla DEF con una sola fila, que almacena el siguiente CÓDIGO disponible (imagine un autonumero simple).

sé lógica, como a continuación presentaría una condición de carrera, en la que dos usuarios podrían terminar con el mismo código:

1) Run a select query to grab next available code from DEF 
2) Insert said code into table ABC 
3) Increment the value in DEF so it's not re-used. 

Sé que, dos usuarios pueden llegar a atascarse en el paso 1), y podría terminar con el mismo CÓDIGO en la tabla ABC.

¿Cuál es la mejor manera de manejar esta situación? Pensé que podría simplemente incluir un "begin tran"/"commit tran" en esta lógica, pero no creo que funcionó. Tenía un procedimiento almacenado como este para probar, pero no evitó la condición de carrera cuando me encontré a partir de dos ventanas diferentes en la EM:

begin tran 

declare @x int 

select @x= nextcode FROM def 

waitfor delay '00:00:15' 

update def set nextcode = nextcode + 1 

select @x 

commit tran 

Puede alguien arrojar alguna luz sobre esto? Pensé que la transacción impediría que otro usuario pueda acceder a mi NextCodeTable hasta que se complete la primera transacción, pero creo que mi comprensión de las transacciones es defectuosa.

EDIT: intenté mover la espera a después de la declaración de "actualización", y obtuve dos códigos diferentes ... pero sospeché eso. Tengo la declaración de espera allí para simular un retraso para que la condición de carrera se pueda ver fácilmente. Creo que el problema clave es mi percepción incorrecta de cómo funcionan las transacciones.

+0

Debe revisar mi respuesta tardía: el aceptado uno no es correcta ... – gbn

Respuesta

6

Establezca el nivel de aislamiento de transacciones en Serializable.
En niveles de aislamiento inferiores, otras transacciones pueden leer los datos en una fila que se lee (pero aún no se modifica) en esta transacción. Entonces dos transacciones pueden leer el mismo valor. A muy bajo aislamiento (la lectura no confirmada) otras transacciones pueden incluso leer los datos después de que ha sido modificada (pero antes de comprometido) ...

los detalles relativos niveles de aislamiento de SQL Server here

Así que la línea de fondo es que el nivel de aislamiento es una pieza crítica aquí para controlar qué nivel de acceso tienen otras transacciones en este.

NOTA. Desde link, aproximadamente Serializable
Las declaraciones no pueden leer los datos que se han modificado pero que aún no han sido confirmados por otras transacciones.
Esto se debe a que los bloqueos se colocan cuando se modifica la fila, no cuando se produce Begin Trans, por lo que lo que ha hecho aún puede permitir que otra transacción lea el valor anterior hasta el momento en que lo modifique.Así que cambiaría la lógica para modificarla en la misma declaración que la leíste, y de ese modo pondré el candado al mismo tiempo.

begin tran 
declare @x int 
update def set @x= nextcode, nextcode += 1 
waitfor delay '00:00:15' 
select @x 
commit tran 
+0

Creo que quería decir * update def set @ x = nextcode, nextcode + = 1 * –

+0

Sí, ¡gracias por eso! –

0

Puede establecer la columna en un valor calculado que se conserva. Esto se ocupará de la condición de carrera.

Persisted Computed Columns

NOTA

uso de este método significa que no es necesario para almacenar el siguiente código en una tabla. La columna de código se convierte en el punto de referencia.

Implementación

Dale la columna las siguientes propiedades bajo especificación columna calculada.

Fórmula = dbo.GetNextCode()

se conserva = Sí

Create Function dbo.GetNextCode() 
Returns VarChar(10) 
As 
Begin 

    Declare @Return VarChar(10); 
    Declare @MaxId Int 

    Select @MaxId = Max(Id) 
    From Table 

    Select @Return = Code 
    From Table 
    Where Id = @MaxId; 

    /* Generate New Code ... */ 

    Return @Return; 

End 
+0

En el cuerpo de la función, ¿cómo se accede a la última (es decir, valor persistido) –

+0

Realmente depende del diseño de la tabla, pero el ejemplo actualizado que doy debe abarcar la mayoría de los casos. – ChaosPandion

+0

Lo sentimos, deberíamos haber hecho dos preguntas en el primer comentario. ¿Cómo persiste el próximo código, o dónde tal vez? –

3

Resumen:

  • Se inició una transacción. Esto en realidad no "hace" nada en sí mismo, modifica el comportamiento posterior
  • Lea los datos de una tabla. El nivel de aislamiento predeterminado es Leído confirmado, por lo que esta instrucción de selección es no forma parte de la transacción.
  • Luego espere 15 segundos
  • A continuación, ejecute una actualización. Con la transacción declarada, esto generará un bloqueo hasta que se comprometa la transacción.
  • A continuación, confirma la transacción y libera el bloqueo.

Así, suponiendo que corrió esta manera simultánea en dos ventanas (A y B):

  • Un leer el valor "al lado" de la tabla def, a continuación, entró en el modo de espera
  • B leyó la mismo valor "siguiente" de la tabla, luego pasó al modo de espera. (Como A solo hizo una lectura, la transacción no bloqueó nada).
  • A actualizó la tabla y probablemente haya realizado el cambio antes de que B saliera del estado de espera.
  • B luego actualizó la tabla, después de que se haya confirmado la escritura de A.

Intenta poner la declaración de espera después de la actualización, antes de la confirmación, y mira qué pasa.

0

Esto es en realidad un problema común en las bases de datos SQL y es por eso que la mayoría (¿todos?) Tienen algunas características incorporadas para solucionar este problema de obtención de un identificador único. Aquí hay algunas cosas que debe considerar si está usando Mysql o Postgres. Si está usando una base de datos diferente, apuesto a que proporcione algo muy similar.

Un buen ejemplo de esto es secuencias de postgres que se puede ver aquí:

Postgres Sequences

MySQL utiliza algo llamado incrementos de automóviles.

Mysql auto increment

1

No es una condición de carrera real. Es más un problema común con las transacciones simultáneas. Una solución es establecer un bloqueo de lectura en la tabla y, por lo tanto, tener una serialización en su lugar.

5

Como han mencionado otros respondedores, puede establecer el nivel de aislamiento para asegurarse de que cualquier cosa que 'leer' el uso de una instrucción SELECT no puede cambiar dentro de una transacción.

Como alternativa, puede obtener un bloqueo en concreto sobre la mesa DEF añadiendo la sintaxis WITH HOLDLOCK después de que el nombre de la tabla, por ejemplo,

SELECT nextcode FROM DEF WITH HOLDLOCK 

no hace mucha diferencia aquí, ya que su transacción es pequeña , pero puede ser útil quitar bloqueos para algunos SELECT y no otros dentro de una transacción. Se trata de "repetibilidad frente a concurrencia".

Un par de documentos MS-SQL relavantes.