【问题标题】:SQL handling ACID and concurrencySQL 处理 ACID 和并发
【发布时间】:2014-07-19 02:30:44
【问题描述】:

(请尽可能笼统地回答。但我在 MS SQL Server 和 MySql 工作,所以如果没有通用答案,请继续回答其中一个或两个。)

考虑在 SQL 数据库中实现的预订系统。我想确保在许多多个用户中,只有一个用户获得预订,而没有其他用户“认为”他们得到了预订。这是数据库工作中的经典并发问题,但我不确定最佳答案是什么。

具体说明:
假设每个用户都有一个 UserID。我们可以想象目前有几个用户正在尝试使用 UserID 值 1004、1005、1009 和 1011 进行预订。

假设资源和预订存储在表 SEA​​TS 中。 我们可以想象 SEATS 表在某一时刻包含:

    ----- SEATS -----------------------------
    SeatID   UserID   ResvTime
       1      1017    2014.07.15 04:17:18.000
       2      NULL    NULL
       3      NULL    NULL
       4      1012    2014.07.15 04:19:35.000
       5      1003    2014.07.15 04:20:46.000
    -----------------------------------------

现在假设“同时”,用户 1004 和 1005 尝试获取 SeatID 3。 我想知道什么 SQL 将正确地确保他们中只有一个获得席位,而另一个获得拒绝。在 T-SQL 中,我能想到的最简单的代码版本是:

  PROC GRABSEAT @seatid INT, @userid INT, @obtained BIT OUTPUT
  BEGIN
     DECLARE @avail INT
     SET @avail = (SELECT UserID FROM SEATS WHERE (SeatID = @seatid))
     IF (@avail IS NULL)
        BEGIN
           UPDATE SEATS SET UserID = @userid, ResvTime = GETDATE() WHERE (SeatID = @seatid)
           SET @obtained = 1
        END
     ELSE
        SET @obtained = 0
  END

但问题是如何防止这种情况允许多个并发用户都执行此 PROC,在同一个座位上获得 TRUE 返回(例如 SeatID = 3)。

例如,如果用户 1004 和 1005 几乎同时执行此 PROC,则他们可以在其中任何一个尝试设置 UserID 列之前执行 SELECT 并获取 @avail = NULL。然后他们都将运行 UPDATE 语句。假设没有更糟的结果,那么其中一个会覆盖另一个的集合,两者都会认为他们获得了席位,但实际上只有最后运行 UPDATE 语句的那个​​才会将他们的数据存储在 SEATS 表中。另一个将覆盖他们的数据。这被称为“丢失输入”问题。但是在 SQL 数据库中防止它的方法是什么?我一直假设每个单独的 SQL 语句都作为 TRANSACTION 执行。一个事务具有四个所谓的“ACID”属性。这些属性是我需要的。所以,我认为在 SQL 数据库中的答案是:

  BEGIN TRANSACTION
  EXCEUTE GRABSEAT @seatid= <id1>, @userid = <id2>, @obtained
  COMMIT

通过这样做,我需要的主要属性(隔离)将保证不会发生我担心的交错执行。

但我看到文章说它根本没有那么简单。我认为各种文章指出的一个大问题是,并非每个 TRANSACTION 都真正以完全原子性和孤立性运行。因此,也许上述在 TRANSACTION 中的包装将无法达到预期的结果。如果没有,那需要什么?

【问题讨论】:

    标签: mysql sql sql-server database acid


    【解决方案1】:

    根据定义,事务是原子的。但是当一个事务的更改对其他用户/连接/事务可见时取决于isolation level。 SQL Server 中的默认隔离是 READ COMMITTED - 请参阅 this question's answer 了解更多信息和有关如何更改它的链接。

    对于这种类型的场景,您可能需要 SERIALIZABLE。好消息是您可以在存储过程中使用SET TRANSACTION ISOLATION LEVEL 语句更改事务的隔离级别。坏消息是,您必须 100% 确定这是代码中唯一会更新SEAT 表的位置。

    从根本上说,您遇到的问题是存在竞争条件。仅仅因为你在一个事务中并不意味着两个事务不能同时调用存储过程,然后运行 ​​SELECT。现在两个 tx 都认为可以进行更新。将隔离级别设置为 SERIALIZABLE 会锁定首先命中 SELECT 的 tx 的表。

    【讨论】:

    • 我忘了SELECT .. FOR UPDATE。这也可能起作用,具体取决于数据库是否支持行级锁定。这是 Postgres 的一些信息 - blog.2ndquadrant.com/…
    【解决方案2】:

    而不是SELECT 语句,为什么不直接进行更新,在NULL 上使用一个额外的过滤器,因此如果值为null,它就无法替换,然后返回查询是否有任何效果.这样,事务是原子的,因为它只是一个查询。

    PROC GRABSEAT @seatid INT, @userid INT, @obtained BIT OUTPUT
    BEGIN
        UPDATE SEATS SET UserID = @userid, ResvTime = GETDATE() 
        WHERE (SeatID = @seatid) AND UserID IS NULL
        SET @obtained = @@ROWCOUNT
    END
    

    由于行锁定,两个更新不能同时发生,所以一个会起作用(返回@@ROWCOUNT = 1,另一个会失败@@ROWCOUNT = 0。

    【讨论】:

    • 这是一个很好的建议。但是,它/删除/我试图用我的人为示例创建的情况。 (对不起,不可能读懂提问者的想法。)如何完全隔离事务(事实证明)是我想要找出的。既然我看到了 AngerClown 给出的答案,而且它是多么简单,我想我可以在问我的问题时节省很多呼吸。
    • 我想我的意思是,如果您使用SELECT 获取用于UPDATE/INSERT 等的信息,您应该考虑重新编写查询,以便您首先不需要SELECT,从而节省了交易的需要。然而,在一个复杂的用例中(假设用户只有在有座位的情况下才会被添加到数据库中),那么 AngerClown 的回答更有意义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    • 1970-01-01
    • 2011-01-11
    • 2011-10-01
    相关资源
    最近更新 更多