SQL Server竞态条件问题

11

(注:以下内容适用于 MS SQL Server)

假设你有一个带有主键身份列和CODE列的表ABC,我们想让每一行都具有基于某种典型校验位公式的唯一、顺序生成的代码。

假设你还有另一张表DEF,只有一行数据,用于存储下一个可用的CODE(可以想象成简单的自动编号)。

我知道像下面这样的逻辑会出现竞争条件,在此情况下两个用户可能会得到相同的CODE:

1) Run a select query to grab next available code from DEF
2) Insert said code into table ABC
3) Increment the value in DEF so it's not re-used.
我知道两个用户可能会被困在步骤1,并最终在ABC表中得到相同的CODE。如何处理这种情况?我认为我可以在此逻辑周围加上“begin tran”/“commit tran”,但我认为那并没有起作用。我有一个存储过程来测试,但当我在MS的两个不同窗口中运行时,我无法避免竞争条件。
begin tran

declare @x int

select   @x= nextcode FROM  def

waitfor delay '00:00:15'

update def set nextcode = nextcode + 1

select @x

commit tran

有人可以给我解释一下吗?我认为事务会防止其他用户在第一个事务完成之前访问我的NextCodeTable,但我想我的事务理解是错误的。

编辑:我尝试将等待移到“更新”语句之后,然后我得到了两个不同的代码…但我怀疑。 我在那里使用waitfor语句来模拟延迟,以便轻松看到竞争条件。 我认为关键问题是我对事务工作方式的错误理解。


你应该查看我的晚回答:被接受的那个不正确... - gbn
7个回答

9
将事务隔离级别设置为Serializable。
在较低的隔离级别下,其他事务可以读取此事务中已读取但尚未修改的行中的数据。因此,两个事务确实可以读取相同的值。在非常低的隔离级别(读取未提交)下,其他事务甚至可以读取已修改但尚未提交的数据...

查看有关SQL Server隔离级别的详细信息here

所以关键是隔离级别在这里是控制其他事务进入此事务的访问级别的关键部分。

注意。从link中了解有关Serializable的详细信息
语句无法读取其他事务已经修改但尚未提交的数据
这是因为当行被修改时放置锁定,而不是在Begin Trans发生时放置锁定,因此您所做的操作可能仍然允许另一个事务读取旧值,直到您修改它的那一点。因此,我建议将其修改为在读取时同时进行修改,从而同时对其进行锁定。

begin tran
declare @x int
update def set @x= nextcode, nextcode += 1
waitfor delay '00:00:15'
select @x
commit tran

我认为你的意思是 update def set @x=nextcode, nextcode += 1 - J Bryan Price

5

正如其他回答者所提到的,您可以设置事务隔离级别,以确保使用SELECT语句“读取”的任何内容在事务中不会发生更改。

或者,您可以通过在表名后添加语法WITH HOLDLOCK来专门锁定DEF表,例如:

SELECT nextcode FROM DEF WITH HOLDLOCK

虽然您的交易规模很小,但在事务中为某些SELECT获取锁定而不为其他SELECT获取锁定可能是有用的。这是“可重复性与并发性”的问题。

以下是一些相关的MS-SQL文档。


5

2

总结:

  • 你开始了一个事务。这本身并没有“做”任何事情,它只修改后续的行为。
  • 你从表中读取数据。默认隔离级别是Read Committed,所以这个select语句不会成为事务的一部分。
  • 然后等待15秒。
  • 然后执行一个更新操作。使用声明的事务,这将生成一个锁,直到事务提交。
  • 然后提交事务,释放锁。

所以,猜测你在两个窗口(A和B)同时运行了这个代码:

  • A从表def中读取了“next”值,然后进入等待模式。
  • B从表中读取了相同的“next”值,然后进入等待模式。(由于A只进行了读取操作,因此该事务没有锁定任何内容。)
  • A然后更新了该表,并可能在B退出等待状态之前提交了更改。
  • B然后在A的写入提交后更新表。

尝试在更新操作之后、提交之前加入等待语句,看看会发生什么。


1

这不是真正的竞争条件,而更多是并发事务中的常见问题。一种解决方案是在表上设置读锁,从而实现序列化。


0

您可以将列设置为持续的计算值。这将解决竞争条件问题。

持续计算列

注意

使用这种方法意味着您不需要在表中存储下一个代码。代码列成为参考点。

实现

在计算列规范下,给列以下属性:

公式 = dbo.GetNextCode()

已持久化 = 是

Create Function dbo.GetNextCode()
Returns VarChar(10)
As
Begin

    Declare @Return VarChar(10);
    Declare @MaxId Int

    Select @MaxId = Max(Id)
    From Table

    Select @Return = Code
    From Table
    Where Id = @MaxId;

    /* Generate New Code ... */

    Return @Return;

End

在函数体内,如何访问最后一个(即持久化的)值? - martin clayton
这真的取决于表的设计,但我提供的更新示例应该涵盖大多数情况。 - ChaosPandion
抱歉 - 应该在第一条评论中问两个问题!下一个代码如何持久化,或者可能在哪里? - martin clayton
请查看我在答案中添加的URL。 - ChaosPandion
这是否解决了竞态条件问题?如果两个客户端同时调用它(或同时从计算列读取),会发生什么? - Dave Cousineau

0

这实际上是SQL数据库中常见的问题,这也是为什么大多数(全部?)数据库都有一些内置功能来解决获取唯一标识符的问题。如果您正在使用Mysql或Postgres,请查看以下内容。如果您使用的是不同的数据库,我敢打赌它们提供非常相似的东西。

一个很好的例子是Postgres序列,您可以在此处查看:

Postgres Sequences

Mysql使用称为自动增量的东西。

Mysql auto increment


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接