检测存储过程中的脏读取

时间:2016-12-16 20:48:49

标签: sql sql-server tsql

我有100个线程,每个线程调用下面定义的存储过程。

如何防止脏读?

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

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
GO

当并行运行时,看起来存储过程的这一部分返回相同的值大约0.01%:

SELECT @NextKey = CerealNumber 
FROM CfgCerealNumber 
WHERE CerealNumberID = @TableID

我通过将更新包装在事务中,以一种阻止脏读取的方式构建我的代码。

如何防止脏读?

8 个答案:

答案 0 :(得分:5)

如果您需要更新并返回更新的内容,那么我只会使用the OUTPUT clause

/*  This program acts like a pedestrian light. If the button is pressed, a light will turn on for a certain moment to let the person pass.

    By   : Dat HA
    Date : 16/09/27
*/

//****************************** VARIABLES ******************************

const int leds[4][12] = //declaring leds - RED, YELLOW, GREEN
{
  //R,Y,G,
  {5, 6, 7},   //north
  {8, 9, 10},  //east
  {11, 12, 13}, //south
  {2, 3, 4}    //west
};


/* Sensors: sensorNumber - itemInArray - sensorDescription - pinDescription

    1 - 0 - pushbutton
    2 - 1 - photocell
    3 - 2 - potentiometer
    4 - 3 - distance      - distanceEcho
    5 - 4 - distance      - distanceTrigger
    6 - 5 - pushbutton (2)
    7 - 6 - servo
*/
const int sensors[]   = {A2, A7, A6, A4, A5, A3, A0}; //pin for each sensor

//******************************   SETUP   ******************************


const int analogPins[6] = {A0, A1, A2, A3, A4, A5}; //for quick pinMode, distanceTrigger we be redeclared as an output is it is an analog input pin

void setup() {

  for (int i = 2; i < 14; i++)     //declaring digital pins as output for leds
  {
    pinMode(i, OUTPUT);
  }

  for (int i = 0; i < 6; i++)      //declaring analog inputs
  {
    pinMode(analogPins[i], INPUT);
  }

  pinMode(sensors[4], OUTPUT);     //declaring trigger pin as an output

}

//****************************** MAIN LOOP ******************************
int green = 5000;
int yellow = 3000;
int aa = 0;


void loop() {

  //north, south
  digitalWrite(leds[0][0], LOW); //red light off
  digitalWrite(leds[2][0], LOW);

  digitalWrite(leds[0][2], HIGH); //green light on
  digitalWrite(leds[2][2], HIGH);

  wait(green);

  digitalWrite(leds[0][2], LOW); //green light off
  digitalWrite(leds[2][2], LOW);

  digitalWrite(leds[0][1], HIGH); //yellow light on
  digitalWrite(leds[2][1], HIGH);

  wait(yellow);

  digitalWrite(leds[0][1], LOW); //yellow light off
  digitalWrite(leds[2][1], LOW);

  digitalWrite(leds[0][0], HIGH); //red light on
  digitalWrite(leds[2][0], HIGH);

  delay(100);

  if (aa == 1)
  {
    flash();
  }

  //*************** SWITCHING SIDES ***************

  digitalWrite(leds[1][0], LOW); //red light off
  digitalWrite(leds[3][0], LOW);

  digitalWrite(leds[1][2], HIGH); //green light on
  digitalWrite(leds[3][2], HIGH);

  wait(green);

  digitalWrite(leds[1][2], LOW); //green light off
  digitalWrite(leds[3][2], LOW);

  digitalWrite(leds[1][1], HIGH); //yellow light on
  digitalWrite(leds[3][1], HIGH);

  wait(yellow);

  digitalWrite(leds[1][1], LOW); //yellow light off
  digitalWrite(leds[3][1], LOW);

  digitalWrite(leds[1][0], HIGH); //red light on
  digitalWrite(leds[3][0], HIGH);

  delay(100);

  if (aa == 1)
  {
    flash();
  }
}

//****************************** FUNCTIONS ******************************
/* Usable functions list:

   LEDS functions - do something to the leds

   ledClear() turn all leds off

   ledRedOn()  turn all red leds on
   ledYelOn()  turn all yellow leds on
   ledGreOn()  turn all green leds on

   SENSORS functions - they will return the apropriate value

   pushButton()     values: 0,1
   photocell()      values: 0-1023
   potentiometer()  values: 0-1023
   distance()       values: 0-200
*/

void ledClear() //turn off all leds
{
  for (int i = 2; i < 14; i++)
  {
    digitalWrite(i, LOW); //turning leds off
  }
}

void ledRedOn()
{
  for (int i = 0; i < 4; i++)
  {
    digitalWrite(leds[i][0], HIGH); //turning red leds on
  }
}

void ledRedOff()
{
  for (int i = 0; i < 4; i++)
  {
    digitalWrite(leds[i][0], LOW); //turning red leds on
  }
}

void ledYelOn()
{
  for (int i = 0; i < 4; i++)
  {
    digitalWrite(leds[i][1], HIGH); //turning yellow leds on
  }
}

void ledGreOn()
{
  for (int i = 0; i < 4; i++)
  {
    digitalWrite(leds[i][2], HIGH); //turning green leds on
  }
}

int pushButton()    // sensor 1
{
  int x = digitalRead(sensors[0]);
  return x; //returning digital value of pushbutton which is either 0 or 1
}

void flash()  //flash the red led
{
  for (int i = 0; i < 12; i++)
  {
    ledGreOn();
    ledRedOn(); //on
    delay(200); //wait
    ledRedOff(); //off
    delay(200); //wait
  }
  ledClear();
  aa = 0;
}

int wait(int x) //see if the wire is disconnected and if so, flash the red led
{
  int i = 0;
  unsigned long currentMillis = millis(); //current time
  while (millis() - currentMillis <= x) //delay
  {
    if (digitalRead(A2) == HIGH) //if wire disconnected
    {
      aa = 1;
    }
  }
}

如果需要进行额外检查,可以在从存储过程返回结果集之前输出到声明的表变量。

另一种方法是首先在表上创建阻塞锁,然后更新:

UPDATE CfgCerealNumber 
SET CerealNumber = CerealNumber + 1 
OUTPUT INSERTED.CerealNumber
WHERE CerealNumberID = @TableID;

但我会把钱放下来,因为我已经看到这仍然会导致问题。我更相信它。

答案 1 :(得分:3)

您可以使用Books Online中所述的@variable = column = expression语法来避免此问题。此外,由于语句在单语句自动事务中执行,因此可以避免显式事务。

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

答案 2 :(得分:2)

您需要替换此声明

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

由此:

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)

这些表提示 READCOMMITTED,READPAST,ROWLOCK 将确保您没有脏读并且没有死锁

它还可以让您决定是否还要进行更新

  

<强> READCOMMITTED
  通过使用锁定或行版本控制,指定读取操作符合READ COMMITTED隔离级别的规则。如果数据库选项READ_COMMITTED_SNAPSHOT为OFF,则数据库引擎会在读取数据时获取共享锁,并在读取操作完成时释放这些锁。如果数据库选项READ_COMMITTED_SNAPSHOT为ON,则数据库引擎不会获取锁并使用行版本控制。

     

<强> READPAST
  指定数据库引擎不读取由其他事务锁定的行。指定READPAST时,将跳过行级锁。也就是说,数据库引擎会跳过行而不是阻止当前事务,直到释放锁。例如,假设表T1包含一个值为1,2,3,4,5的整数列。如果事务A将值3更改为8但尚未提交,则SELECT * FROM T1(READPAST)产生值1,2,4,5。READPAST主要用于在实现使用SQL Server表的工作队列时减少锁争用。使用READPAST的队列读取器会跳过其他事务锁定的队列条目到下一个可用队列条目,而不必等到其他事务释放其锁定。

     

<强> ROWLOCK
  指定在通常采用页锁或表锁时执行行锁。在以SNAPSHOT隔离级别操作的事务中指定时,除非将ROWLOCK与需要锁定的其他表提示(例如UPDLOCK和HOLDLOCK)组合,否则不会进行行锁定。

Source MSDN Table Hints (Transact-SQL)

您可能还需要使用UPDLOCK和/或HOLDLOCK

答案 3 :(得分:0)

sp_getapplock将确保事务具有独占锁。更新和读取将在下一个线程可以使用之前提交,因此不会有任何脏读。

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

答案 4 :(得分:0)

开始事务/提交事务 将确保您没有脏读

性能存在缺陷,如果从另一个事务内部运行该过程,则在提交最多外部事务之前不会释放写锁定。这将序列化所有线程并阻止并发。

请参阅此示例(假设执行需要很长时间):

begin tran
    ...
    exec GetNextCerealIdentity ... ; -- the write lock is established
    ...
commit tran -- the write lock is released

可以在事务结束之前释放 ,但您必须使用程序创建应用程序锁 GetNextCerealIdentity 过程中的sp_getAppLock sp_releaseAppLock

这可能非常棘手,您必须注意或者您可以同时拥有死锁或某些脏读

您必须在程序开始时执行 sp_getAppLock ,并在结束时执行 sp_releaseAppLock (在返回之前。在您的示例中,您有很多返回所以你必须在许多点释放锁定)

如果发生错误,请不要忘记释放锁定。锁将在事务结束时释放,但您希望在程序结束时释放它! : - )

您必须确保应用程序锁定是唯一一个持有计数器(CfgCerealNumber)的表。

通常,SQL Server会对表进行写锁定,并会干扰您的锁定,因为写入锁定将在事务结束时释放,而不是在程序结束时释放。

您必须将过程更改为事务级别READ UNCOMMITED,以便代码中的UPDATE不会生成写锁定。记得在释放应用程序锁的同一时间回到COMMITTED。

如果您在独占模式下获得锁定,您将确保只有一个连接可以执行表CfgCerealNumber上的更新/选择

您可以为锁提供您想要的任何名称。我使用了与表(CfgCerealNumber)相同的名称,但这并不重要。最重要的是,您必须为初始 get 和您放入代码中的所有 release 使用相同的名称。

ALTER procedure GetNextCerealIdentity(@NextKey int output,@TableID int)
AS
declare @RowCount int, @Err int
set nocount on
select  @NextKey = 0

-- replace begin tran with:    
EXEC sp_getapplock @Resource = 'CfgCerealNumber', @LockMode = 'Exclusive';  
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

/*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)
    -- replace Rollback Transaction with:
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    EXEC sp_releaseapplock @Resource = 'CfgCerealNumber';  
    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

    -- replace Rollback Transaction with:
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    EXEC sp_releaseapplock @Resource = 'CfgCerealNumber';  

    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)
    -- replace Rollback Transaction with:
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    EXEC sp_releaseapplock @Resource = 'CfgCerealNumber';  
    set nocount off
    return 1
    end

-- replace commit transaction with:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
EXEC sp_releaseapplock @Resource = 'CfgCerealNumber';  

set nocount off
return 0
GO

如果你改变这样的过程,我之前的例子将不会出现并发问题:

begin tran
    ...
    exec GetNextCerealIdentity ... ; -- the lock is established AND released
    ...
commit tran -- common "write locks" are released

一个可能的补充是使用 BEGIN / END TRY .. BEGIN / END CATCH 构造,以便在出现意外异常的情况下释放锁定(这将给另一个专家:你将从程序中退出一个单点,这样您就可以在一个单点上放置指令以释放锁定并放回先前的事务隔离级别。

请参阅以下链接: (sp_getAppLock)https://msdn.microsoft.com/en-us/library/ms189823.aspx和 (sp_releaseAppLock)https://technet.microsoft.com/en-us/library/ms178602.aspx

答案 5 :(得分:0)

一种选择是使用sp_getapplock系统存储过程并使用sql server的内置锁定来确保对资源的序列化访问。

var d = new Date();
d.setMonth(d.getMonth() - 1); //1 month ago
db.data.find({created:{$gte:d}}); //change "data" for your collection's name

答案 6 :(得分:0)

培根比特击败了我,但使用OUTPUT条款将是解决赛车问题的最简单方法。当然锁定也是一种选择,虽然我认为它的开销会略高一些。也就是说,使用IDENTITY列或SEQUENCE比尝试手动实现此功能要容易得多。

我冒昧地将答案放在你的代码中并添加一些评论:

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

备注:

  • 退出存储过程之前无需再次执行SET NOCOUNT OFF。当您超出范围时,此设置将返回到您输入存储过程之前的状态。
  • 我不确定你是否需要WITH (UPDLOCK),但肯定不会受到伤害。
  • 我保持事务处理的时间尽可能短,没有理由从事务中的table-variable中获取值。
  • 我认为首先执行ROLLBACK然后执行RaisError()会更安全,因为后者可能导致连接被某些客户端软件删除和/或您可能在TRY...CATCH内。两者都会破坏命令的流程,并且最终会导致事务计数不匹配。
  • YMMV但我总是被告知在出现错误时使用否定返回码。正回程代码可能用于表示行数。虽然我从来没有见过在实践中使用过的行。

答案 7 :(得分:0)

如前所述,您可以使用自动增量内置功能,例如标识列或序列。

如果您不想这样做,则需要以串行方式访问该表:使用应用程序锁定或其他功能。

例如,您可以将提示添加到表格的 FIRST 访问权限(在交易中),如下所示:

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

这将保证在所有并行线程中顺序访问表。