【问题标题】:Oracle select for update behaviourOracle 选择更新行为
【发布时间】:2013-01-31 07:24:42
【问题描述】:

我们试图解决的问题是这样的。

  • 我们有一个表格,里面有代表卡片的行。预订交易的目的是为客户分配一张卡
  • 一张卡不能属于多个客户
  • 一段时间后(如果未购买),必须将卡归还到可用资源池中
  • 多位客户可同时预订
  • 我们使用 Oracle 数据库来存储数据,因此解决方案必须至少在 Oracle 11 上运行

我们的解决方案是为卡分配一个状态,并存储它的预订日期。保留卡时,我们使用“选择更新”语句来完成。该查询查找可用卡和很久以前保留的卡。

但是我们的查询没有按预期工作。

我准备了一个简化的情况来解释这个问题。 我们有一个 card_numbers 表,里面充满了数据——所有的行都有非空的 ID 号。 现在,让我们尝试锁定其中的一些。

-- first, in session 1
set autocommit off;

select id from card_numbers  
where id is not null  
and rownum <= 1  
for update skip locked;

我们这里不提交事务,行必须被锁定。

-- later, in session 2
set autocommit off;

select id from card_numbers  
where id is not null  
and rownum <= 1  
for update skip locked;

预期的行为是,在两个会话中,我们得到一个满足查询条件的不同行。

但它不是那样工作的。取决于我们是否使用查询的“跳过锁定”部分 - 行为变化:

  • 没有“跳过锁定” - 第二个会话被阻止 - 等待第一个会话中的事务提交或回滚
  • 带有“跳过锁定” - 第二个查询立即返回空结果集

所以,在这个冗长的介绍之后,问题来了。

在 Oracle 中是否可以实现所需的锁定行为?如果是,那么我们做错了什么?什么是正确的解决方案?

【问题讨论】:

    标签: sql oracle locking blocking


    【解决方案1】:

    this blog note 中描述了您遇到的 FOR UPDATE SKIP LOCKED 行为。我的理解是 FOR UPDATE 子句是在 WHERE 子句之后评估的。 SKIP LOCKED 就像一个额外的过滤器,它保证在本应返回的行中,没有一个被锁定。

    你的语句在逻辑上等价于:从card_numbers中找到第一行,如果没有锁定则返回。显然这不是你想要的。

    这是一个重现您描述的行为的小测试用例:

    SQL> CREATE TABLE t (ID PRIMARY KEY)
      2  AS SELECT ROWNUM FROM dual CONNECT BY LEVEL <= 1000;
    
    Table created
    
    SESSION1> select id from t where rownum <= 1 for update skip locked;
    
            ID
    ----------
             1
    
    SESSION2> select id from t where rownum <= 1 for update skip locked;
    
            ID
    ----------
    

    第二次选择没有返回任何行。您可以使用光标来解决此问题:

    SQL> CREATE FUNCTION get_and_lock RETURN NUMBER IS
      2     CURSOR c IS SELECT ID FROM t FOR UPDATE SKIP LOCKED;
      3     l_id NUMBER;
      4  BEGIN
      5     OPEN c;
      6     FETCH c INTO l_id;
      7     CLOSE c;
      8     RETURN l_id;
      9  END;
     10  /
    
    Function created
    
    SESSION1> variable x number;
    SESSION1> exec :x := get_and_lock;
    
    PL/SQL procedure successfully completed
    x
    ---------
    1
    
    SESSION2> variable x number;
    SESSION2> exec :x := get_and_lock;
    
    PL/SQL procedure successfully completed
    x
    ---------
    2
    

    因为我已经明确地获取了游标,所以只会返回一行(并且只有一行会被锁定)。

    【讨论】:

    • 您提到的博客说的有些不同:某些行没有返回不是因为它们被锁定,而是因为该行所在块上的 ITL 插槽已用尽。增加 ITL 插槽的数量解决了他们案例中的问题。但我不想排除您提出的代码也可以提供帮助。我的经验是“ROWNUM
    • @codo:我同意情况不同,但主要行为是相同的:即使某些行可用,Oracle 返回的行数也少于询问的行数。我的理解是,这是因为 SKIP 子句是在 WHERE 子句之后评估的。在我测试的情况下,添加 ITL 插槽没有帮助。
    • 谢谢你,文森特。这种方法似乎适用于我所描述的情况。然而,事实证明,我们需要在单个选择中锁定多行以进行更新。当使用您的方法并在两个单独的会话中加载几行时,ID 号开始重叠。
    • @mateusz:不应该有重叠:你不能同时锁定一行两次。也许您发现了一个错误,或者更有可能该行已被锁定然后被释放(事务结束),然后再次被锁定。也许您可以构建一个测试用例来重现您的发现?
    • +1 表示“意外后果”博文。这对我很有帮助!
    【解决方案2】:

    虽然其他答案已经充分解释了使用各种SELECT .. FOR UPDATE 变体在您的数据库中发生了什么,但我认为值得一提的是,Oracle 不鼓励直接使用FOR UPDATE SKIP LOCKED,而是鼓励使用Oracle AQ

    http://download.oracle.com/docs/cd/B28359_01/server.111/b28286/statements_10002.htm#i2066346

    我们在应用程序中使用Oracle AQ,我可以确认,经过一段陡峭的学习曲线后,它可以是一种直接在数据库中处理生产者/消费者的非常方便的方法

    【讨论】:

    • 不能使用Oracle AQ怎么办?
    • @svlada:嗯,这个问题真的不能简单地回答:-)。为什么不创建一个包含所有详细信息的新 Stack Overflow 问题?
    【解决方案3】:

    并不是说文森特的回答是错误的,但我会以不同的方式设计它。

    我的第一反应是选择更新第一个可用记录并使用“reserved_date”更新记录。在 XXX 时间过去且事务未完成后,将记录的 reserved_date 更新回 null 以再次释放记录。

    我尽量让事情变得简单。对我来说,这更简单。

    【讨论】:

    • 文森特的回答是解决这个特定问题的一个很好的提示,所以我会选择它作为正确的。悬停我的投票也投给了你,因为我们也决定这样做。此外,我们将事务登录从 Java 移至 PL/SQL 过程,以使其尽可能接近数据库。这样交易应该很短。
    猜你喜欢
    • 1970-01-01
    • 2011-09-14
    • 2015-06-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-14
    • 2020-08-27
    相关资源
    最近更新 更多