为啥在选择的更新跳过锁定时,若有的几个会话可以看到行?

Posted

技术标签:

【中文标题】为啥在选择的更新跳过锁定时,若有的几个会话可以看到行?【英文标题】:Why row is visible to several sessions when selected FOR UPDATE SKIP LOCKED?为什么在选择的更新跳过锁定时,若有的几个会话可以看到行? 【发布时间】:2019-07-19 15:42:14 【问题描述】:

假设有两个表TST_SAMPLE (10000 rows)TST_SAMPLE_STATUS (empty)

我想遍历TST_SAMPLE 中的每条记录,并相应地将一条记录添加到TST_SAMPLE_STATUS

在一个简单的线程中:

begin
  for r in (select * from TST_SAMPLE)
  loop 

    insert into TST_SAMPLE_STATUS(rec_id, rec_status)
    values (r.rec_id, 'TOUCHED');

  end loop;

  commit;
end;
/

在多线程解决方案中有一种情况,我不清楚。 那你能解释一下是什么原因导致处理一行TST_SAMPLE多次。

请参阅下面的详细信息。

create table TST_SAMPLE(
  rec_id       number(10) primary key 
);

create table TST_SAMPLE_STATUS(
  rec_id       number(10),
  rec_status   varchar2(10),
  session_id   varchar2(100)
);


begin
  insert into TST_SAMPLE(rec_id)
  select LEVEL from dual connect by LEVEL <= 10000;

  commit;
end;
/


CREATE OR REPLACE PROCEDURE tst_touch_recs(pi_limit int) is
  v_last_iter_count int;
begin

   loop

     v_last_iter_count := 0;

     --------------------------
     for r in (select *
                 from TST_SAMPLE A
                where rownum < pi_limit
                  and NOT EXISTS (select null
                                    from TST_SAMPLE_STATUS B
                                   where B.rec_id = A.rec_id)
                  FOR UPDATE SKIP LOCKED)
     loop

        insert into TST_SAMPLE_STATUS(rec_id, rec_status, session_id)
        values (r.rec_id, 'TOUCHED', SYS_CONTEXT('USERENV', 'SID'));

        v_last_iter_count := v_last_iter_count + 1;
     end loop;

     commit;
     --------------------------

     exit when v_last_iter_count = 0;

   end loop;
end;
/

FOR-LOOP 中,我尝试遍历以下行: - 没有状态(NOT EXISTS 子句) - 当前未锁定在另一个线程中(FOR UPDATE SKIP LOCKED)

对于迭代中的确切行数没有要求。 这里pi_limit 只是一个批次的最大尺寸。唯一需要做的就是在一个会话中处理TST_SAMPLE 的每一行。

所以让我们在 3 个线程中运行这个过程。

declare
 v_job_id number;
begin

  dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
  dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);
  dbms_job.submit(v_job_id, 'begin tst_touch_recs(100); end;', sysdate);

  commit;
end;

出乎意料的是,我们看到一些行在多个会话中处理

select count(unique rec_id) AS unique_count,
       count(rec_id)        AS total_count
  from TST_SAMPLE_STATUS;


| unique_count | total_count |
------------------------------
|        10000 |       17397 |
------------------------------


-- run to see duplicates
select * 
  from TST_SAMPLE_STATUS 
 where REC_ID in (
                    select REC_ID 
                      from TST_SAMPLE_STATUS
                     group by REC_ID
                    having count(*) > 1
                 )
 order by REC_ID;

请帮助识别程序执行过程中的错误tst_touch_recs

【问题讨论】:

听起来你可能需要某种排队系统。看看Oracle's Advanced Queuing(它比看起来更简单!您创建一个表来保存队列数据,在其上创建一个队列,然后根据需要将消息入队和出队,瞧!) It seems FOR UPDATE + SKIP LOCKED 不会一次锁定所有记录,而是在循环期间依次锁定每一行,因此当您通过游标循环并锁定行时,另一个事务可以锁定一行,将行插入 TST_SAMPLE_STATUS 并在提交时释放锁(在循环之后)。因此,您可以获取并锁定已由另一个事务处理的行。所以尝试一次获取并锁定行到一个表变量中,然后在循环中遍历它。 @valex,在您提供的 AskTOM 帖子中,有一个短语“将在您打开光标时锁定所有行”。肯定是这样,因为我已经测试过这个案例,发现了几个博客和文章,支持这一点。不过,谢谢,我会尝试一次获取所有行。 @Boneist,可能整个 AQ 环境对于我的小目的来说非常庞大。 @diziaq 整句:“open cursor for select ... for update 将在您打开游标的那一刻锁定所有行。但是, open cursor for select ... for update skip locked;不锁定 any 行。它们在您 fetch" 时被锁定 【参考方案1】:

这里有一个小例子,说明为什么要读取行两次。

在两个会话中运行以下代码,在第一个会话后几秒钟开始第二个:

declare
  cursor c is 
    select a.*
     from TST_SAMPLE A
    where rownum < 10
      and NOT EXISTS (select null
                        from TST_SAMPLE_STATUS B
                       where B.rec_id = A.rec_id)
      FOR UPDATE SKIP LOCKED;

  type rec is table of c%rowtype index by pls_integer;
  rws rec;
begin
  open c; -- data are read consistent to this time

  dbms_lock.sleep ( 10 );

  fetch c 
  bulk  collect 
  into  rws;

  for i in 1 .. rws.count loop
    dbms_output.put_line ( rws(i).rec_id );
  end loop;

  commit;

end;
/

您应该会看到两个会话显示相同的行。

为什么?

因为 Oracle 数据库具有语句级一致性,所以当您打开游标时,两者的结果集都会被冻结。

但是当您有 SKIP LOCKED 时,FOR UPDATE 锁定只会在when you fetch the rows 中起作用。

所以会话 1 启动并找到不在 TST_SAMPLE_STATUS 中的前 9 行。然后等待 10 秒。

如果您在这 10 秒内启动会话 2,光标将寻找相同的九行

此时没有行被锁定。

现在,有趣的地方来了。

第一个会话的睡眠将结束。然后它将获取行,锁定它们并跳过任何已经锁定的行。

不久之后,它就会提交。 释放锁

片刻之后,会话 2 开始读取这些行。此时锁定

所以没有什么可以跳过的。

你如何解决这个问题取决于你想要做什么。

假设您不能转向基于集合的方法,您可以通过添加使事务可序列化:

set transaction isolation level serializable;

游标循环之前。然后这将转移到事务级别的一致性。使数据库能够在获取行时检测“某些已更改”。

但是您需要在外部循环中捕获 ORA-08177: can't serialize access for this transaction 错误。否则任何重新读取相同行的进程都会在此时退出。

或者,正如评论者所建议的那样,使用高级队列。

【讨论】:

以上是关于为啥在选择的更新跳过锁定时,若有的几个会话可以看到行?的主要内容,如果未能解决你的问题,请参考以下文章

选择以行限制锁定更新跳过

选择更新锁定

为啥我的NVIDIA 垂直同步里没怎么自适应

如何在 postgres 中使用“更新跳过锁定”而不锁定查询中使用的所有表中的行?

关键词若create在sqlyog中编辑,若输入正确,则显示为啥颜色

为啥我不能跳过这个 div 指令?