【问题标题】:atomic compare and swap in a database数据库中的原子比较和交换
【发布时间】:2010-12-03 04:17:33
【问题描述】:

我正在研究一种工作排队解决方案。我想查询数据库中的给定行,其中状态列具有特定值,修改该值并返回该行,并且我想原子地执行它,这样其他查询就不会看到它:

begin transaction select * from table where pk = x and status = y update table set status = z where pk = x commit transaction --(the row would be returned)

必须不可能有 2 个或更多并发查询返回该行(一个查询执行会在其状态 = y 时看到该行)——有点像一个互锁的 CompareAndExchange 操作。

我知道上面的代码可以运行(用于 SQL 服务器),但是交换总是原子的吗?

我需要一个适用于 SQL Server 和 Oracle 的解决方案

【问题讨论】:

  • 您的队列解决方案是否需要同时在 Oracle 和 SQL Server 中工作?如果只有 Oracle,您可能应该研究 Oracle Advanced Queuing。
  • @Stephen Nope - 它必须与两者一起使用。我们销售此软件,并支持两个后端。我们发现我们最好的策略是不依赖于特定产品的特性,并且尽可能只使用 ANSI SQL 语法(尽管我们可以在必要时为我们的语法加上大小写括号)。

标签: sql sql-server database oracle


【解决方案1】:

PK 是主键吗?那么这不是问题,如果您已经知道主键就没有运动。如果 pk 主键,那么这就引出了一个明显的问题如何你知道要出列的项目的 pk...

问题在于,如果您知道主键并希望将下一个“可用”(即 status = y)出队并将其标记为出队(删除它或设置 status = z)。

执行此操作的正确方法是使用单个语句。不幸的是,Oracle 和 SQL Server 的语法不同。 SQL Server 语法是:

update top (1) [<table>]
set status = z 
output DELETED.*
where  status = y;

我对 Oracle 的 RETURNING 子句不够熟悉,无法举一个类似于 SQL 的 OUTPUT 的示例。

其他 SQL Server 解决方案要求 SELECT(使用 UPDLOCK)上的锁定提示正确。 在 Oracle 中,首选途径是使用 FOR UPDATE,但在 SQL Server 中不起作用,因为 FOR UPDATE 将与 SQL 中的游标一起使用。

无论如何,您在原始帖子中的行为是不正确的。多个会话都可以选择相同的行,甚至都可以更新它,将相同的出列项返回给多个阅读器。

【讨论】:

  • 荣誉。 OUTPUT 是在单个命令中完成他正在寻找的事情的好方法。
  • @Remus:我会看看这个。我们经常知道 pk——我不依赖轮询作为我的在线通知机制,即项目已提交到队列。读取队列的服务收到远程消息。 RPC 通知包括 pk,但不包括有效负载,因为有效负载可能很大。但是,如果服务出现故障,它必须从表中恢复。我正在尝试消除潜在的竞争条件,恢复服务可能会从表中加载新条目,并接收远程通知。
  • @JMarsh:我明白了。如果我理解正确,大多数访问将通过 PK 完成,但每隔一段时间(在服务恢复期间)将根据其他一些标准(以及隐含的其他索引顺序)完成访问。这很难正确解决,您可能会因为索引访问顺序而遇到死锁,请参阅rusanu.com/2009/05/16/readwrite-deadlock(至少在 SQL Server 上)。
  • @Remus:这是一篇非常有趣的文章!我认为这里要避免使用数据库来控制并发。我正在考虑在数据库中不需要 CAS 的中间层算法。我非常强烈地考虑在 RPC 通知中包含有效负载,从而完全避免竞争条件(也许利用 MSMQ 来传递通知和有效负载)。
  • 确保不要把婴儿和洗澡水一起扔出去。无论数据库中存在什么并发问题,它们都可能存在于中间层中。谁在传递您的通知?也许与其更多地移动到中间层,您可以更多地移动到数据库(即使用 SQL 自己的消息传递msdn.microsoft.com/en-us/library/ms166104.aspx)。
【解决方案2】:

我有一些遵循类似模式的应用程序。有一张像您这样的表格,代表工作队列。该表有两个额外的列:thread_id 和 thread_date。当应用程序从队列中请求工作时,它会提交一个线程 ID。然后单个更新语句使用提交的 id 更新所有适用的行,线程 id 列和当前时间的线程日期列。在该更新之后,它会选择具有该线程 ID 的所有行。这样您就不需要声明显式事务。 “锁定”发生在初始更新中。

thread_date 列用于确保您不会得到孤立的工作项。如果从队列中拉出项目然后您的应用程序崩溃会发生什么?您必须能够再次尝试这些工作项。因此,您可能会从队列中抓取所有尚未标记为已完成但已分配给具有遥远过去线程日期的线程的项目。由你来定义“遥远”。

【讨论】:

    【解决方案3】:

    作为一般规则,要进行这样的原子操作,您需要确保在执行选择时设置排他(或更新)锁,以便在更新之前没有其他事务可以读取该行。

    典型的语法是这样的:

     select * from table where pk = x and status = y for update
    

    但你需要查一下才能确定。

    【讨论】:

    • 注意,这样做不会阻止另一个会话读取该行,除非其他会话也使用“for update”子句。
    【解决方案4】:

    试试这个。验证在 UPDATE 语句中。

    代码

    IF EXISTS (SELECT * FROM sys.tables WHERE name = 't1')
        DROP TABLE dbo.t1
    GO
    CREATE TABLE dbo.t1 (
        ColID       int         IDENTITY,
        [Status]    varchar(20)
    )
    GO
    
    DECLARE @id             int
    DECLARE @initialValue   varchar(20)
    DECLARE @newValue       varchar(20)
    
    SET @initialValue = 'Initial Value'
    
    INSERT INTO dbo.t1 (Status) VALUES (@initialValue)
    SELECT @id = SCOPE_IDENTITY()
    
    SET @newValue = 'Updated Value'
    
    BEGIN TRAN
    
    UPDATE dbo.t1
    SET
        @initialValue = [Status],
        [Status]      = @newValue
    WHERE ColID    = @id
      AND [Status] = @initialValue
    
    SELECT ColID, [Status] FROM dbo.t1
    
    COMMIT TRAN
    
    SELECT @initialValue AS '@initialValue', @newValue AS '@newValue'
    

    结果

    ColID Status
    ----- -------------
        1 Updated Value
    
    @initialValue @newValue
    ------------- -------------
    Initial Value Updated Value
    

    【讨论】:

      猜你喜欢
      • 2021-03-09
      • 1970-01-01
      • 2023-03-24
      • 1970-01-01
      • 1970-01-01
      • 2013-08-30
      • 2011-10-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多