2012-03-19 17 views
6

Tengo una tabla con números secuenciales (piense en números de factura o ID de estudiante).Acceso concurrente a la base de datos: evitando que dos usuarios obtengan el mismo valor

En algún momento, el usuario necesita solicitar el número anterior (para calcular el siguiente número). Una vez que el usuario conoce el número actual, necesita generar el siguiente número y agregarlo a la tabla.

Mi preocupación es que dos usuarios puedan generar erróneamente dos números idénticos debido al acceso concurrente.

He oído hablar de procedimientos almacenados, y sé que esa podría ser una solución. ¿Hay una mejor práctica aquí para evitar problemas de concurrencia?

Editar: Esto es lo que tengo hasta ahora:

USE [master] 
GO 

SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
ALTER PROCEDURE [dbo].[sp_GetNextOrderNumber] 
AS 
BEGIN 
    BEGIN TRAN 

DECLARE @recentYear INT 
DECLARE @recentMonth INT 
DECLARE @recentSequenceNum INT 

-- SET NOCOUNT ON added to prevent extra result sets from 
-- interfering with SELECT statements. 
SET NOCOUNT ON; 

    -- get the most recent numbers 
SELECT @recentYear = Year, @recentMonth = Month, @recentSequenceNum = OrderSequenceNumber 
FROM dbo.OrderNumbers 
WITH (XLOCK) 
WHERE Id = (SELECT MAX(Id) FROM dbo.OrderNumbers) 

    // increment the numbers 
IF (YEAR(getDate()) > IsNull(@recentYear,0)) 
    BEGIN 
     SET @recentYear = YEAR(getDate()); 
     SET @recentMonth = MONTH(getDate()); 
     SET @recentSequenceNum = 0; 
    END 
ELSE 
    BEGIN 
     IF (MONTH(getDate()) > IsNull(@recentMonth,0)) 
      BEGIN 
       SET @recentMonth = MONTH(getDate()); 
       SET @recentSequenceNum = 0; 
      END 
     ELSE 
      SET @recentSequenceNum = @recentSequenceNum + 1; 
    END 

-- insert the new numbers as a new record 
INSERT INTO dbo.OrderNumbers(Year, Month, OrderSequenceNumber) 
VALUES (@recentYear, @recentMonth, @recentSequenceNum) 

COMMIT TRAN 
END 

Esto parece funcionar, y me da los valores que quiero. Hasta ahora, aún no he agregado ningún bloqueo para evitar el acceso concurrente.

Editar 2: Se agregó WITH(XLOCK) para bloquear la tabla hasta que finalice la transacción. No voy a tener un rendimiento aquí. Siempre y cuando no se agreguen entradas duplicadas y no se produzcan bloqueos, esto debería funcionar.

Respuesta

6

sabes que SQL Server hace eso por ti, ¿verdad? Puede incluir una columna de identidad si necesita un número secuencial o una columna calculada si necesita calcular el nuevo valor en función de otro.

Pero, si eso no resuelve su problema, o si necesita hacer un cálculo complicado para generar su nuevo número que no se puede hacer en un simple inserto, sugiero escribir un procedimiento almacenado que traba la tabla, obtiene el último valor, genera el nuevo, lo inserta y luego desbloquea la tabla.

Leer este link para aprender sobre nivel de aislamiento

sólo asegúrese de que el período de "bloqueo" lo más pequeño posible

+0

El número que generaré consiste en el año (3 dígitos), seguido del mes (2 dígitos), seguido de un número de 3 dígitos que se restablece a cero cada mes. Así que algo así como 012 03 000. –

+0

dependiendo de dónde provenga ese número de 3 dígitos, puede resolverlo fácilmente con una columna calculada. De lo contrario, utilice un procedimiento – Diego

+0

El número de 3 dígitos simplemente se incrementa desde el número anterior (a menos que el número anterior sea del mes anterior, luego el número es 0 nuevamente) –

0

La forma en que manejamos esto en SQL Server es mediante el uso de la sugerencia de tabla UPDLOCK en una sola transacción.

Por ejemplo:

INSERT 
    INTO MyTable (
     MyNumber , 
     MyField1) 
    SELECT IsNull(MAX(MyNumber), 0) + 1 , 
     "Test" 
    FROM MyTable WITH (UPDLOCK) 

No es bastante, pero ya que nos proporcionó el diseño de base de datos y no pueden cambiar debido a las aplicaciones heredadas para acceder a la base de datos, ésta era la mejor solución que podríamos llegar a.

+0

¿Ejecutado como un procedimiento almacenado? –

+0

La nuestra, pero ciertamente no es un requisito. Es el SQL lo que importa, no cómo se ejecuta. –

3

Aquí está un ejemplo de implementación de contador. La idea básica es usar el activador de inserción para actualizar los números de, por ejemplo, las facturas.El primer paso es crear una tabla para guardar un valor de último número asignado:

create table [Counter] 
(
    LastNumber int 
) 

e inicializar con una sola fila:

insert into [Counter] values(0) 

mesa de la factura de la muestra:

create table invoices 
(
    InvoiceID int identity primary key, 
    Number varchar(8), 
    InvoiceDate datetime 
) 

Procedimiento almacenado LastNumber primero actualiza la fila del contador y luego recupera el valor. Como el valor es un int, simplemente se devuelve como valor de retorno del procedimiento; de lo contrario, se requeriría una columna de salida. El procedimiento toma como un número de parámetro los siguientes números a buscar; salida es el último número.

create proc LastNumber (@NumberOfNextNumbers int = 1) 
as 
begin 
    declare @LastNumber int 

    update [Counter] 
     set LastNumber = LastNumber + @NumberOfNextNumbers -- Holds update lock 
    select @LastNumber = LastNumber 
    from [Counter] 
    return @LastNumber 
end 

Trigger en la tabla factura recibe número de facturas insertadas al mismo tiempo, pide siguientes n números del procedimiento y actualizaciones almacenadas facturas con que los números.

create trigger InvoiceNumberTrigger on Invoices 
after insert 
as 
    set NoCount ON 

    declare @InvoiceID int 
    declare @LastNumber int 
    declare @RowsAffected int 

    select @RowsAffected = count(*) 
    from Inserted 
    exec @LastNumber = dbo.LastNumber @RowsAffected 

    update Invoices 
    -- Year/month parts of number are missing 
     set Number = right ('000' + ltrim(str(@LastNumber - rowNumber)), 3) 
    from Invoices 
     inner join 
     (select InvoiceID, 
       row_number() over (order by InvoiceID desc) - 1 rowNumber 
      from Inserted 
     ) insertedRows 
      on Invoices.InvoiceID = InsertedRows.InvoiceID 

En caso de una reversión, no habrá espacios vacíos. La mesa auxiliar podría expandirse fácilmente con teclas para diferentes secuencias; en este caso, una fecha válida, hasta que sea bueno, ya que puede preparar esta tabla de antemano y dejar que LastNumber se preocupe por seleccionar el contador para el año/mes actual.

Ejemplo de uso:

insert into invoices (invoiceDate) values(GETDATE()) 

A medida que se genera automáticamente el valor de la columna número, uno debe volver a leerlo. Creo que EF tiene provisiones para eso.

Cuestiones relacionadas