none
SET TRANSACTION ISOLATION LEVEL para gestiona un contador de máximo. RRS feed

  • Pregunta

  • Hola,

    les planteo mi problema. Tengo una gestión de venta de productos asociados a un identificador de tiquet, todos los productos de una misma compra ("carrito") se cargan con el mismo identificador de tiquet. Para buscar el identificador de tiquet hago primero una consulta que me devuelve el máximo. Mi problema es que según como funcione la concurrencia entre transacciones podría estar asignando el mismo identificador de tiquet a compras ("carritos") diferentes.

    He hecho algunas pruebas de funcionamiento de transacciones para averiguar si esto se puede solucionar con bloqueos. En principio me parecía que con el nivel SERIALIZABLE bloquearía y estaría solucionado, pero veo que no es así.

    Os paso el ejemplo:

    DECLARE @id_tiquet AS int

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION t1

    -- cogemos el máximo de identificador de tiquet
    SELECT @id_tiquet = MAX(id_tiquet) FROM dbo.movimientos 
    SET @id_tiquet = @id_tiquet + 1


    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION t2

    SELECT @id_tiquet = MAX(id_tiquet) FROM dbo.movimientos 
    SET @id_tiquet = @id_tiquet + 1

    INSERT INTO [dbo].[movimientos] ([id_tiquet], [id_usuario], [id_producto], [unidades], [importe])
    VALUES (@id_tiquet, 1, 1, 1, 0.03)

    INSERT INTO [dbo].[movimientos] ([id_tiquet], [id_usuario], [id_producto], [unidades], [importe])
    VALUES (@id_tiquet, 1, 1, 2, 0.06)


    COMMIT TRANSACTION t2


    INSERT INTO [dbo].[movimientos] ([id_tiquet], [id_usuario], [id_producto], [unidades], [importe])
    VALUES (@id_tiquet, 2, 1, 1, 0.03)

    INSERT INTO [dbo].[movimientos] ([id_tiquet], [id_usuario], [id_producto], [unidades], [importe])
    VALUES (@id_tiquet, 2, 1, 2, 0.06)


    COMMIT TRANSACTION t1

    La idea es que la transacción 2 se interpone antes de finalizar la transacción 1 y en este caso todos los movimientos insertados tienen el mismo identificador de tiquet en lugar de un identificador de tiquet diferente para cada transacción.

    No se si lo estoy planteando bien y explicando bien, cualquier ayuda o sugerencia me sería de mucha ayuda. Gracias de antemano.

    miércoles, 19 de julio de 2017 9:48

Respuestas

Todas las respuestas

  • Hola.

    Si entendí bien el planteamiento, el tiquet debe estar enlistado en la transacción que hace el INSERT. Es decir, no veo porque se hace otra transacción. Si lo que se quiere es que el sistema atienda usuarios concurrentes, pues debes abrir o iniciar una transacción, obtener el tiquet, hacer el INSERT y cerrar o finalizar la transacción.

    Saludos,


    Guillermo Taylor F.
    MVP Data Platform & IT Pro
    Mi Blog

    miércoles, 19 de julio de 2017 11:45
  • Efectivamente como dice Guillermo, para garantizar el tiquet no hay necesidad de abrir una nueva transacción, todo debería estar enmarcado en el mismo bloque transaccional para garantizar el numero de tiquet.

    Saludos.


    Mariano K.

    miércoles, 19 de julio de 2017 12:39
  • Que tal si creas una tabla solo para mantener la secuencia?

    http://sqlmag.com/sql-server/t-sql-puzzle-solution-custom-sequence

    Este otro link habla sobre la carrera entre leer de la misma tabla donde se va a insertar en un ambiente con concurrencia.

    http://www.dbdelta.com/conditional-insertupdate-race-condition/

    Algo asi como:

    insert into dbo.movimientos (col1, ..., coln)
    select max(col1) + 1, @col2, @col3, ..., @coln
    from dbo.movimientos (updlock, holdlock);

    Deberas probar la solucion ya que al igual que setear el nivel de transaccion a SERIALIZABLE toda otra session realizando una operacion no compatible tendra que esperar.

    Para evitar un scan de tabla y/o indice, se debera tener un indice apropiado para soportar la busqueda.


    AMB

    Some guidelines for posting questions...

    AYÚDANOS A AYUDARTE, guía básica de consejos para formular preguntas

    miércoles, 19 de julio de 2017 12:42
  • Gracias por tus sugerencias.

    Respecto a usar otra tabla para mantener la secuencia, en mi caso no tengo claro que solucione nada. He mirado el ejemplo que me has puesto, no sé si lo entiendo bien. Matizar que uso SQL Server 2005 y que en mi caso tengo que insertar varios registros con el mismo identificador,  (todos los productos de un mismo carrito tienen el mismo identificador de tiquet), aunque use otra tabla en transacciones concurrentes podría obtener secuencias erróneas. 

    En el ejemplo que me pasas que hace el incremento todo en la misma sentencia, está bien pero necesito hacer diferentes inserts con el mismo identificador.

    Respecto al tema de bloqueos uplock, holdlock, no lo veo muy claro. Igualmente no me importa usar SERIALIZABLE porque la concurrencia es entre pocos usuarios y no es crítico el tiempo de ejecución.

    En realidad sólo quería saber si la integridad del contador es correcta usando una transacción SERIALIZABLE o incluso bajando a REPEATABLE READ.

    Muchas gracias por tu tiempo,

    Elena.


    ElenaL

    jueves, 20 de julio de 2017 11:12
  • Hola,

    es que no lo he explicado bien. La lectura del máximo del contador y el insert lo hago en la misma transacción, evidentemente. El ejemplo que he puesto es una simulación de que pasaría si en ese instante entra en ejecución otra transacción, quizás es que no es correcto intentar simular transacciones concurrentes de esta forma. 

    En realidad la trasacción sería:

    DECLARE @id_tiquet AS int

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION t1

    -- cogemos el máximo de identificador de tiquet
    SELECT @id_tiquet = MAX(id_tiquet) FROM dbo.movimientos 
    SET @id_tiquet = @id_tiquet + 1

    INSERT INTO [dbo].[movimientos] ([id_tiquet], [id_usuario], [id_producto], [unidades], [importe])
    VALUES (@id_tiquet, 2, 1, 1, 0.03)

    INSERT INTO [dbo].[movimientos] ([id_tiquet], [id_usuario], [id_producto], [unidades], [importe])
    VALUES (@id_tiquet, 2, 1, 2, 0.06)

    COMMIT TRANSACTION t1

    Pero qué pasa si entre medio entra otra transacción que hace exactamente lo mismo, es lo que intentaba hacer con el ejemplo de la transacción 2.

    Perdón si no me explico bien y muchas gracias.


    ElenaL

    jueves, 20 de julio de 2017 11:14
  • Hola.

    Un escenario para probar y comprobar cual TRANSACTION ISOLATION LEVEL es el que estás buscando o se adecua a tus necesidades, es mediante SQL Server Management Studio, pero con diferentes sesiones. Puede ser con el mismo usuario en diferentes ventanas o con diferentes usuarios.

    SQL Server, por lo menos así lo entiendo, cuando está escribiendo, siempre adquiere un LOCK exclusivo hasta que se completa la transacción. Cuando está leyendo, salvo otro SET TRANSACTION ISOLATION LEVEL, siempre está en READ COMMITTED, lo que pudiese permitir que sesiones distintas obtuviesen un conjunto de datos resultado diferente.

    Sugiero esta buena lectura, Questions About T-SQL Transaction Isolation Levels You Were Too Shy to Ask, entre otras que están en la documentación en línea de SQL Server, para entender mejor el tema y ajustar de acuerdo con las necesidades. Adicionalmente, para tu programa, recuerda que la recomendación es obtener la conexión lo más tarde que se pueda, generar la transacción, enlistar las operaciones necesarias en ésta, cerrar la transacción y liberar la conexión.

    Saludos,


    Guillermo Taylor F.
    MVP Data Platform & IT Pro
    Mi Blog

    jueves, 20 de julio de 2017 12:00
  • Hola Elena, concuerdo mucho con lo que indican los otros colegas.. tu requerimiento se puede manejar con una sola transacción. Pero tengo mis dudas con respecto al planteamiento. Por lo que veo,  generas un id a partir de el último id  existente +1. Pero adicionalmente veo que ingresas los datos de productos, cantidades y usuario en duro. Es decir, eso no debería pasar a través de parámetros?.   Lo  que es necesario  saber es como trabajas esto a nivel de aplicación, por lo que hasta donde yo se, sólo podemos pasar 1 registro a la vez a no ser de que se capturare el id, lo devuelvas a la aplicación y a través de algún bucle recorrer tu lista de productos en movimiento y hagas los insert correspondientes.  

    Ahora, podrías crear una tabla anexa que te permita generar y almacenar  los números de ticket, pues ahí realizas el mismo ejercicio de tu transacción (me refiero a como lo generas el id) y luego lo inserta en la tabla, esto te ayudaría a que cuando un usuario inserte otros movimientos, genere un nuevo id sin que considere el que ya está trabajando la aplicación con otro usuario.


    Saludos desde Chile.





    jueves, 20 de julio de 2017 12:09
  • Gracias, haré pruebas con diferentes sesiones y me leeré lo que me has pasado que lo explica muy bien.

    ElenaL

    jueves, 20 de julio de 2017 12:13
  • > en mi caso tengo que insertar varios registros con el mismo identificador,  

    > (todos los productos de un mismo carrito tienen el mismo identificador de tiquet)

    Pudieras pasar todos los productos del carrito como un documento XML e insertarlos en una sola sentencia.  El uso de parametros tipo tabla se introdujo desde la version 2008.


    AMB

    Some guidelines for posting questions...

    AYÚDANOS A AYUDARTE, guía básica de consejos para formular preguntas

    • Marcado como respuesta Moderador M martes, 25 de julio de 2017 21:22
    jueves, 20 de julio de 2017 13:30