为什么选择 FOR UPDATE SKIP LOCKED 时行对多个会话可见?
Why row is visible to several sessions when selected FOR UPDATE SKIP LOCKED?
假设有两个表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
。
这里有一个小例子可以说明为什么要读取行两次。
运行 以下代码在两个会话中,在第一个之后几秒开始第二个:
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;
游标循环之前。然后这将转向事务级别的一致性。使数据库能够在获取行时检测 "something changed"。
但是您需要在外循环中捕获 ORA-08177: can't serialize access for this transaction
错误。或者任何重新读取相同行的进程都会在此时退出。
或者,正如评论者所建议的那样,使用高级队列。
假设有两个表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
。
这里有一个小例子可以说明为什么要读取行两次。
运行 以下代码在两个会话中,在第一个之后几秒开始第二个:
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;
游标循环之前。然后这将转向事务级别的一致性。使数据库能够在获取行时检测 "something changed"。
但是您需要在外循环中捕获 ORA-08177: can't serialize access for this transaction
错误。或者任何重新读取相同行的进程都会在此时退出。
或者,正如评论者所建议的那样,使用高级队列。