Detecting dirty reads from a stored procedure

前端 未结 8 522
自闭症患者
自闭症患者 2021-02-04 02:27

I\'ve got 100 threads that are each calling the stored procedure as defined below.

How do I prevent dirty reads?

SET QUOTED_IDENTIFIER OFF
SET ANSI_NULLS         


        
相关标签:
8条回答
  • 2021-02-04 02:56

    You can avoid the problem by using the @variable = column = expression syntax as described in the Books Online. Also, since the statement executes in an single-statement automatic transaction, you can avoid explicit transaction.

    SET QUOTED_IDENTIFIER ON;
    SET ANSI_NULLS ON;
    GO
    
    CREATE PROCEDURE GetNextSerialIdentity
          @NextKey int output
        , @TableID int
    AS
    SET NOCOUNT ON;
    
    UPDATE dbo.CfgSerialNumber
    SET @NextKey = SerialNumber = SerialNumber + 1
    WHERE SerialNumberID = @TableID;
    
    IF @@ROWCOUNT = 0
    BEGIN
    RAISERROR ('No Table Record Exists in CfgCerealNumber for ID:%d   ', 
                      16,1, @TableID);
    END
    GO
    
    0 讨论(0)
  • 2021-02-04 02:58

    You need to replace this statement

    UPDATE CfgCerealNumber Set CerealNumber = CerealNumber + 1  
    WHERE CerealNumberID = @TableID
    

    by this:

    declare @CerealNumber int
    
    SELECT @CerealNumber = CerealNumber  + 1
    FROM CfgCerealNumber WITH (READCOMMITTED, READPAST, ROWLOCK) 
    WHERE CerealNumberID = @TableID
    
    if @CerealNumber is not null
        UPDATE CfgCerealNumber Set CerealNumber = @CerealNumber
        WHERE CerealNumberID = @TableID
    else
        raiserror ('Row was locked by another update (no dirty read and no deadlock happen) or no Table Record Exists in CfgCerealNumber for ID:%d   ', 
                  16,1, @TableID)
    

    these table hints READCOMMITTED, READPAST, ROWLOCK will make sure that you have no dirty read and no deadlock

    it will also let you decide if you still want to do an update

    READCOMMITTED
    Specifies that read operations comply with the rules for the READ COMMITTED isolation level by using either locking or row versioning. If the database option READ_COMMITTED_SNAPSHOT is OFF, the Database Engine acquires shared locks as data is read and releases those locks when the read operation is completed. If the database option READ_COMMITTED_SNAPSHOT is ON, the Database Engine does not acquire locks and uses row versioning.

    READPAST
    Specifies that the Database Engine not read rows that are locked by other transactions. When READPAST is specified, row-level locks are skipped. That is, the Database Engine skips past the rows instead of blocking the current transaction until the locks are released. For example, assume table T1 contains a single integer column with the values of 1, 2, 3, 4, 5. If transaction A changes the value of 3 to 8 but has not yet committed, a SELECT * FROM T1 (READPAST) yields values 1, 2, 4, 5. READPAST is primarily used to reduce locking contention when implementing a work queue that uses a SQL Server table. A queue reader that uses READPAST skips past queue entries locked by other transactions to the next available queue entry, without having to wait until the other transactions release their locks.

    ROWLOCK
    Specifies that row locks are taken when page or table locks are ordinarily taken. When specified in transactions operating at the SNAPSHOT isolation level, row locks are not taken unless ROWLOCK is combined with other table hints that require locks, such as UPDLOCK and HOLDLOCK.

    Source MSDN Table Hints (Transact-SQL)

    You might also need to use UPDLOCK and/or HOLDLOCK

    0 讨论(0)
  • 2021-02-04 03:01

    As already mentioned, you can use the auto increment built-in functionality, such as identity columns or sequence.

    If you don't want this, you need to make the access to the table in serial manner: using application lock or other abilities.

    For example, you can add the hints to the FIRST access to the table (in the transaction) like the below:

    UPDATE CfgCerealNumber
    Set CerealNumber = CerealNumber + 1
    FROM CfgCerealNumber with (tablockx, holdlock)
    WHERE CerealNumberID = @TableID
    

    This will guarantee the sequential access to the table in all parallel threads.

    0 讨论(0)
  • 2021-02-04 03:02

    One option would be to use the sp_getapplock system stored procedure and use the built in locking of sql server to ensure serialized access to a resource.

    CREATE PROC MyCriticalWork(@MyParam INT)      
    AS
        DECLARE @LockRequestResult INT
        SET @LockRequestResult=0
    
        DECLARE @MyTimeoutMiliseconds INT
        SET @MyTimeoutMiliseconds=5000--Wait only five seconds max then timeouit
    
        BEGIN TRAN
    
        EXEC @LockRequestResult=SP_GETAPPLOCK 'MyCriticalWork','Exclusive','Transaction',@MyTimeoutMiliseconds
        IF(@LockRequestResult>=0)BEGIN
    
            /*
            DO YOUR CRITICAL READS AND WRITES HERE
            */
    
            --Release the lock
            COMMIT TRAN
        END ELSE
            ROLLBACK TRAN   
    
    0 讨论(0)
  • 2021-02-04 03:03

    sp_getapplock will ensure that the transaction has an exclusive lock. The updates and reads will commit before the next thread can use it, so there cannot be any dirty reads.

    SET QUOTED_IDENTIFIER OFF
    SET ANSI_NULLS OFF
    GO
    
        ALTER procedure GetNextCerealIdentity(@NextKey int output,@TableID int)
        AS
        declare @RowCount int, @Err int
        set nocount on
        select  @NextKey = 0
        begin transaction
       --ADDED CODE
        EXEC sp_getapplock @Resource='MyLock', @LockMode='Exclusive'
                    , @LockOwner='Transaction', @LockTimeout = 15000
        Again:
        /*Update CfgCerealNumber Table */
        UPDATE CfgCerealNumber Set CerealNumber = CerealNumber + 1  WHERE CerealNumberID = @TableID
        select  @RowCount = @@RowCount, @Err = @@Error      /*Obtain updated Cereal number previously incremented*/
    
        if @Err <> 0                            /* If Error gets here then exit         */
            begin                        
            raiserror ('GetNextCerealIDSeries Failed with Error: %d TableID: %d ', 
                   16,1, @Err, @TableID)
                    Rollback Transaction    
            set nocount off
            return 1
            end
    
        if @RowCount = 0                        /* No Record then assume table is not   */
                                        /* been initialized for TableID Supplied*/
            begin
            raiserror ('No Table Record Exists in CfgCerealNumber for ID:%d   ', 
                          16,1, @TableID)
            set nocount off
                    Rollback Transaction
            return 1
            end
    
        /*Obtain updated Cereal number previously incremented*/
        SELECT @NextKey = CerealNumber 
         From CfgCerealNumber WHERE CerealNumberID = @TableID
    
        select   @Err = @@Error                     /*Obtain updated Cereal number previously incremented*/
    
        if @Err <> 0                            /* If Error gets here then exit         */
            begin                        
            raiserror ('GetNextCerealIDSeries Failed with Error: %d TableID: %d ', 
                   16,1, @Err, @TableID)
                    Rollback Transaction    
            set nocount off
            return 1
            end
    
        commit transaction
        set nocount off
        return 0
    
    0 讨论(0)
  • 2021-02-04 03:09

    Bacon Bits beat me to it, but using the OUTPUT clause will be the easiest way to work around your racing issue. Off course locking is an option too, although I think it will have slightly higher overhead. That said, using an IDENTITY column or a SEQUENCE is so much easier than trying to implement this functionality manually.

    I took the liberty of putting the answer in your code and add a few remarks:

    SET QUOTED_IDENTIFIER OFF
    SET ANSI_NULLS OFF
    GO
    
    ALTER procedure GetNextCerealIdentity(@NextKey int output,@TableID int)
    AS
    set nocount on
    
    DECLARE @RowCount int, @Err int
    DECLARE @output TABLE (NextKey int)
    
    begin transaction
    
        /*Update CfgCerealNumber Table */
        UPDATE CfgCerealNumber WITH (UPDLOCK) 
           Set CerealNumber = CerealNumber + 1
        OUTPUT inserted.CerealNumber INTO @output (NextKey)
         WHERE CerealNumberID = @TableID
    
        select @RowCount = @@RowCount, /*Obtain updated Cereal number previously incremented*/ 
               @Err = @@Error      
    
        if @Err <> 0                            /* If Error gets here then exit         */
            begin                        
                Rollback Transaction    
                raiserror ('GetNextCerealIDSeries Failed with Error: %d TableID: %d ', 16,1, @Err, @TableID)
                return -1
            end
    
        if @RowCount = 0                        /* No Record then assume table is not   */
                                        /* been initialized for TableID Supplied*/
            begin
                Rollback Transaction
                raiserror ('No Table Record Exists in CfgCerealNumber for ID:%d   ', 16,1, @TableID)
                return -1
            end
    
    COMMIT TRANSACTION
    
    
    /*Obtain updated Cereal number previously incremented*/
    SELECT @NextKey = NextKey 
     From @output
    
    return 0
    GO
    

    Remarks:

    • There is no need to do SET NOCOUNT OFF again before exiting the stored procedure. As you go out of scope this setting will return back to what it was before you entered the stored procedure.
    • I'm not sure you need the WITH (UPDLOCK), but it certainly won't hurt.
    • I kept the transaction open for as short as possible, there is no reason to fetch the value from the table-variable inside the transaction.
    • I think it's safer to first do a ROLLBACK and then do a RaisError() simply because the latter might cause the connection to be dropped by some client-software and/or you might be inside a TRY...CATCH. Both will break the flow of the commands and you'll end up with transaction count mismatch.
    • YMMV but I've always been told to use negative return codes in case of an error. Positive return-codes might be used to indicate the number of rows.. although I've never ever seen that one used in practice.
    0 讨论(0)
提交回复
热议问题