我有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
我通过将更新包装在事务中,以一种阻止脏读取的方式构建我的代码。
如何防止脏读?
答案 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)
,但肯定不会受到伤害。ROLLBACK
然后执行RaisError()
会更安全,因为后者可能导致连接被某些客户端软件删除和/或您可能在TRY...CATCH
内。两者都会破坏命令的流程,并且最终会导致事务计数不匹配。答案 7 :(得分:0)
如前所述,您可以使用自动增量内置功能,例如标识列或序列。
如果您不想这样做,则需要以串行方式访问该表:使用应用程序锁定或其他功能。
例如,您可以将提示添加到表格的 FIRST 访问权限(在交易中),如下所示:
UPDATE CfgCerealNumber
Set CerealNumber = CerealNumber + 1
FROM CfgCerealNumber with (tablockx, holdlock)
WHERE CerealNumberID = @TableID
这将保证在所有并行线程中顺序访问表。