假设有两个表 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 子句)
- 当前未锁定在另一个线程中(用于更新跳过锁定)
迭代中不需要确切的行数。
这里
pi_limit
只是一个批次的最大尺寸。唯一需要的是处理 TST_SAMPLE
的每一行在一个 session 中。所以让我们在 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;
出乎意料的是,我们看到一些行在几个 session 中被处理
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
.
最佳答案
这是一个小例子,说明为什么要读取行两次。
在两个 session 中运行以下代码,在第一个 session 后几秒钟开始第二个 session :
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;
/
您应该会看到两个 session 都显示相同的行。
为什么?
由于 Oracle 数据库具有语句级一致性,因此当您打开游标时,两者的结果集都会被卡住。
但是当你有 SKIP LOCKED 时,FOR UPDATE 锁定只会在 when you fetch the rows 中起作用。 .
因此 session 1 启动并找到不在 TST_SAMPLE_STATUS 中的前 9 行。然后等待 10 秒。
如果您在这 10 秒内启动 session 2,光标将查找相同的 9 行。
此时没有行被锁定。
现在,这就是它变得有趣的地方。
第一个 session 中的 sleep 将结束。然后它将获取行,锁定它们并跳过任何已经锁定的行。
很快,它就会提交。释放锁。
片刻之后, session 2 开始读取这些行。此时行是 不是 锁定!
所以没有什么可以跳过的。
你究竟如何解决这个问题取决于你想要做什么。
假设您无法转向基于集合的方法,您可以通过添加以下内容使事务可序列化:
set transaction isolation level serializable;
在游标循环之前。这将转移到事务级一致性。使数据库能够在获取行时检测“某些更改”。
但是你需要捕获
ORA-08177: can't serialize access for this transaction
您在外循环中的错误。或者任何重新读取相同行的进程此时都会退出。或者,正如评论者所建议的那样使用高级排队。
关于sql - 选择 FOR UPDATE SKIP LOCKED 时,为什么行对多个 session 可见?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54879711/