强制 Oracle 返回带有 SKIP LOCKED 的 TOP N 行
Posted
技术标签:
【中文标题】强制 Oracle 返回带有 SKIP LOCKED 的 TOP N 行【英文标题】:Force Oracle to return TOP N rows with SKIP LOCKED 【发布时间】:2011-09-01 07:33:23 【问题描述】:有一个fewquestions关于如何在Oracle和SQL Server中实现一个类似队列的表(锁定特定行,选择一定数量的行,跳过当前锁定的行)。
假设至少有 N
行符合条件,我如何保证检索到一定数量的 (N
) 行?
据我所知,Oracle 在确定要跳过哪些行之前应用了WHERE
谓词。这意味着如果我想从表中提取一行,并且两个线程同时执行相同的 SQL,一个将接收该行,另一个将接收一个空结果集(即使有更多符合条件的行)。
这与 SQL Server 处理UPDLOCK
、ROWLOCK
和READPAST
锁定提示的方式相反。在 SQL Server 中,TOP
似乎神奇地限制了在成功获得锁后的记录数。
注意,两个有趣的文章here和here。
甲骨文
CREATE TABLE QueueTest (
ID NUMBER(10) NOT NULL,
Locked NUMBER(1) NULL,
Priority NUMBER(10) NOT NULL
);
ALTER TABLE QueueTest ADD CONSTRAINT PK_QueueTest PRIMARY KEY (ID);
CREATE INDEX IX_QueuePriority ON QueueTest(Priority);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (1, NULL, 4);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (2, NULL, 3);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (3, NULL, 2);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (4, NULL, 1);
在两个单独的会话中,执行:
SELECT qt.ID
FROM QueueTest qt
WHERE qt.ID IN (
SELECT ID
FROM
(SELECT ID FROM QueueTest WHERE Locked IS NULL ORDER BY Priority)
WHERE ROWNUM = 1)
FOR UPDATE SKIP LOCKED
注意第一个返回一行,第二个会话不返回一行:
会话 1
ID ---- 4第 2 节
ID ----SQL 服务器
CREATE TABLE QueueTest (
ID INT IDENTITY NOT NULL,
Locked TINYINT NULL,
Priority INT NOT NULL
);
ALTER TABLE QueueTest ADD CONSTRAINT PK_QueueTest PRIMARY KEY NONCLUSTERED (ID);
CREATE INDEX IX_QueuePriority ON QueueTest(Priority);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 4);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 3);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 2);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 1);
在两个单独的会话中,执行:
BEGIN TRANSACTION
SELECT TOP 1 qt.ID
FROM QueueTest qt
WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE Locked IS NULL
ORDER BY Priority;
请注意,两个会话都返回不同的行。
会话 1
ID ---- 4第 2 节
ID ---- 3如何在 Oracle 中获得类似的行为?
【问题讨论】:
我将赏金给可以给我一个比 Gary Myers 更简单的答案的人,因为我也想省略光标,就像 OP 一样 【参考方案1】:“据我所知,Oracle 在确定要跳过哪些行之前应用了 WHERE 谓词。”
是的。这是唯一可能的方法。在确定结果集之前,您不能从结果集中跳过一行。
答案就是不限制 SELECT 语句返回的行数。您仍然可以使用 FIRST_ROWS_n 提示来指示优化器您不会抓取完整的数据集。
调用 SELECT 的软件应该只选择前 n 行。在 PL/SQL 中,它会是
DECLARE
CURSOR c_1 IS
SELECT /*+FIRST_ROWS_1*/ qt.ID
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY
FOR UPDATE SKIP LOCKED;
BEGIN
OPEN c_1;
FETCH c_1 into ....
IF c_1%FOUND THEN
...
END IF;
CLOSE c_1;
END;
【讨论】:
FIRST_ROWS_N 和 FIRST_ROWS(N) 不限制返回的结果,但优化查询引擎以更快地返回这些行。这会导致 all 行被锁定,同时执行时仍会产生类似的行为(第一个线程将锁定所有行,第二个线程将跳过锁定并且什么也看不到) 再看一遍,由于您使用的是游标,因此在根据我在rwijk.blogspot.com/2009/02/for-update-skip-locked.html 找到的游标从游标中获取之前,不会锁定该行。我试图在没有 PL/SQL 的情况下解决这个问题(我想从 JDBC 执行一个简单的 SQL 批处理)。 这个答案很有趣。使用游标跳过锁定行的可能性可能是一个选项,特别是如果游标从存储过程作为REF CURSOR
返回(如果这甚至可能的话)。但和 OP 一样,我更倾向于在单个 SELECT
语句中寻找类似 SQL-Server 的解决方案【参考方案2】:
Gary Meyers 发布的解决方案几乎是我能想到的,除了使用 AQ,它可以为您完成所有这些以及更多。
如果您真的想避免使用 PLSQL,您应该能够将 PLSQL 转换为 Java JDBC 调用。您需要做的就是准备相同的 SQL 语句,执行它,然后继续对其进行单行提取(或 N 行提取)。
http://download.oracle.com/docs/cd/B10501_01/java.920/a96654/resltset.htm#1023642 的 Oracle 文档提供了一些如何在语句级别执行此操作的线索:
要设置查询的获取大小,请在执行查询之前对语句对象调用 setFetchSize()。如果将提取大小设置为 N,则每次访问数据库都会提取 N 行。
所以你可以用 Java 编写一些看起来像(在伪代码中)的东西:
stmt = Prepare('SELECT /*+FIRST_ROWS_1*/ qt.ID
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY
FOR UPDATE SKIP LOCKED');
stmt.setFetchSize(10);
stmt.execute();
batch := stmt.fetch();
foreach row in batch
-- process row
commit (to free the locks from the update)
stmt.close;
更新
根据下面的 cmets,建议使用 ROWNUM 来限制收到的结果,但在这种情况下不起作用。考虑这个例子:
create table lock_test (c1 integer);
begin
for i in 1..10 loop
insert into lock_test values (11 - i);
end loop;
commit;
end;
/
现在我们有一个有 10 行的表。请注意,我已经小心地以相反的顺序插入了行,包含 10 的行是第一个,然后是 9 等等。
假设您想要前 5 行,按升序排列 - 即从 1 到 5。您的第一次尝试是这样的:
select *
from lock_test
where rownum <= 5
order by c1 asc;
结果如下:
C1
--
6
7
8
9
10
这显然是错误的,而且几乎是每个人都会犯的错误!查看查询的解释计划:
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5 | 65 | 4 (25)| 00:00:01 |
| 1 | SORT ORDER BY | | 5 | 65 | 4 (25)| 00:00:01 |
|* 2 | COUNT STOPKEY | | | | | |
| 3 | TABLE ACCESS FULL| LOCK_TEST | 10 | 130 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(ROWNUM<=5)
Oracle 自下而上执行计划 - 请注意,对 rownum 的过滤是在排序之前执行的,Oracle 按照它找到它们的顺序获取行(它们在此处插入的顺序 10, 9, 8, 7, 6),在达到 5 行后停止,然后对该集合进行排序。
因此,要获得正确的前 5 个,您需要先进行排序,然后使用内联视图进行排序:
select * from
(
select *
from lock_test
order by c1 asc
)
where rownum <= 5;
C1
--
1
2
3
4
5
现在,终于进入正题了 - 你能把 for update 跳过锁定在正确的位置吗?
select * from
(
select *
from lock_test
order by c1 asc
)
where rownum <= 5
for update skip locked;
这给出了一个错误:
ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc
试图将 for 更新移动到视图中会出现语法错误:
select * from
(
select *
from lock_test
order by c1 asc
for update skip locked
)
where rownum <= 5;
唯一可行的是以下,会给出错误的结果:
select *
from lock_test
where rownum <= 5
order by c1 asc
for update skip locked;
事实上,如果你在会话 1 中运行这个查询,然后在会话 2 中再次运行,会话 2 将给出零行,这真的是错误的!
那你能做什么?打开游标并从中获取您想要的行数:
set serveroutput on
declare
v_row lock_test%rowtype;
cursor c_lock_test
is
select c1
from lock_test
order by c1
for update skip locked;
begin
open c_lock_test;
fetch c_lock_test into v_row;
dbms_output.put_line(v_row.c1);
close c_lock_test;
end;
/
如果您在会话 1 中运行该块,它将打印出“1”,因为它锁定了第一行。然后在会话 2 中再次运行它,它会打印“2”,因为它跳过了第 1 行并获得了下一个空闲行。
这个例子是在 PLSQL 中,但是在 Java 中使用 setFetchSize 你应该能够得到完全相同的行为。
【讨论】:
你确定这会起作用吗? FIRST_ROWS 提示将导致 Oracle 尝试尽快返回第一行,但我相信无论提示中的数字如何,它最终仍会锁定所有行(提示实际上并不限制记录数检索),或 JDBC 中设置的提取大小。 我不确定它会起作用,但我认为它会起作用。在这个线程的某个地方,有人说 Oracle 只在返回时锁定行。如果您的 fetch 大小为 10,那么一次调用 fetch 应该给您 10 行并锁定这 10 行,并且不再锁定。我会说你将不得不编写一些代码并尝试一下。我会这样做,但我的 Java 技能很生疏。 我没有尝试过setFetchSize(10)
,但限制了ROWNUM <= 10
。这具有相同的效果,只是在使用ROWNUM
时优先级排序毫无用处。在设置锁定标志之后提交确实可以解决问题,但是在 EJB SessionBean 处理业务逻辑的中间提交并不是很好。而且它仍然不能很好地扩展,即第二个进程在第一个提交之前运行FOR UPDATE SKIP LOCKED
查询的概率仍然太高......但我想这是没有使用 AQ 的最佳解决方案......
我不认为使用 ROWNUM 来限制结果会给你想要的。我已经更新了上面的答案以包含更多信息 - 基本上,我认为您需要使用 setFetchSize。
更新后,我认为这归结为原始解决方案,但更详细。感谢您提供额外的研究/信息。【参考方案3】:
在您的第一个会话中,当您执行时:
SELECT qt.ID
FROM QueueTest qt
WHERE qt.ID IN (
SELECT ID
FROM
(SELECT ID FROM QueueTest WHERE Locked IS NULL ORDER BY Priority)
WHERE ROWNUM = 1)
FOR UPDATE SKIP LOCKED
您的内部选择尝试仅抓取 id=4 并将其锁定。这是成功的,因为这一行还没有被锁定。
在第二个会话中,您的内部选择仍然尝试抓取 ONLY id=4 并将其锁定。这不成功,因为该单行仍被第一个会话锁定。
现在,如果您在第一个会话中更新了“锁定”字段,则运行该选择的下一个会话将获取 id=3。
基本上,在您的示例中,您依赖于未设置的标志。要使用您的锁定标志,您可能意味着执行以下操作:
-
根据某些标准选择您想要的 ID。
立即更新这些 ID 的锁定标志 = 1(如果资源繁忙,另一个会话会在此步骤中超过 1 个或多个 ID,请再次转到 1)
对这些 ID 执行任何操作
将锁定标志更新回 null
然后您可以使用您的 select for update 跳过锁定语句,因为您的锁定标志正在维护。
就个人而言,我不喜欢标志的所有更新(您的解决方案可能出于任何原因需要它们),所以我可能只是 try 选择我想要更新的 ID(通过任何标准)在每个会话中:
select * from queuetest where ... for update skip locked;
例如(实际上,我的标准不会基于 id 列表,但 queuetest 表过于简单):
sess 1:从 queuetest 中选择 * (4,3) 中的 id 用于更新跳过锁定;
sess 2:从 queuetest 中选择 * id in (4,3,2) 用于更新跳过锁定;
这里 sess1 会锁定 4,3 而 sess2 只会锁定 2。
据我所知,您不能在 select for update 语句中执行 top-n 或使用 group_by/order_by 等,您将得到 ORA-02014。
【讨论】:
并发队列的意义在于允许多个客户端处理符合某些条件的记录。该处理比仅更新行中的数据更复杂。典型用法是多个客户端尝试使用指定的完全相同的条件访问行。在您的示例中,第一个会话可能会锁定所有记录,使会话 2 挨饿。 如果 2 个客户端想要访问和锁定由完全相同的条件指定的完全相同的行,那么一个会“赢”(锁定这些行),另一个会“输”(让资源忙) )。在我的示例中,会话 1 尝试锁定 2 行并成功锁定 2 行,会话 2 尝试锁定 3 行并仅成功锁定 1。您不能让多个会话锁定相同的行。 @Travis:我认为您在 OP 示例(对于 Oracle)中缺少的是您从未设置“锁定”字段,但您尝试在子选择中使用它。对于两个会话,它与 ... where id IN (4) ... 因为 "SELECT ID FROM (SELECT ID FROM QueueTest WHERE Locked IS NULL ORDER BY Priority) WHERE ROWNUM = 1" 将始终返回 4 如果您是'不更新表中的锁定标志。显然 SqlServer 允许您在锁定选择中执行前 n 个操作,但据我所知,Oracle 不允许使用 select 进行更新的组函数,您会得到 ORA-02014,所以我尝试展示 alt 重点是我希望每个客户端只锁定一行,而不是全部。是的,锁定第一行会获胜,在这种情况下,我希望第二行锁定 next 行(参见 SQL Server 示例)。锁定字段在帖子中不相关,因为我想在事务中获得行锁定后设置它,这将允许它从稍后出现的其他查询中排除。我现在正在检查的问题是在那之前发生的事情(两个客户端尝试同时获得访问权限,在任何行上设置 Locked 标志之前)。 锁定字段是相关的,因为它在您的子选择中使用。如果你想使用它,你需要设置它(参见我的OP中的步骤1-4)。如果您不想使用它,下一个问题是选择您希望锁定的 ID 组的标准是什么。我的回复的第二部分(项目符号)我为每个会话选择了一个简单的 ID 列表,但您可以使用日期字段或真实表格中的任何内容。据我所知,您不能将此子选择与“更新”一起使用 top-n 或类似的组功能。那么,您选择锁定哪些 ID 的标准是什么?【参考方案4】:我的解决方案 - 是这样编写存储过程:
CREATE OR REPLACE FUNCTION selectQueue
RETURN SYS_REFCURSOR
AS
st_cursor SYS_REFCURSOR;
rt_cursor SYS_REFCURSOR;
i number(19, 0);
BEGIN
open st_cursor for
select id
from my_queue_table
for update skip locked;
fetch st_cursor into i;
close st_cursor;
open rt_cursor for select i as id from dual;
return rt_cursor;
END;
这是一个简单的示例 - 返回 TOP FIRST 非阻塞行。要检索 TOP N 行 - 将单次提取替换为局部变量 ("i") 与循环提取到临时表中。
PS:返回光标 - 用于休眠友谊。
【讨论】:
【参考方案5】:我遇到了这个问题,我们花了很多时间来解决它。有的使用for update
for update skip locked
,在oracle 12c中,一个新的方法是使用fetch first n rows only
。但是我们使用的是 oracle 11g。
最后,我们尝试了这个方法,发现效果很好。
CURSOR c_1 IS
SELECT *
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY;
myRow c_1%rowtype;
i number(5):=0;
returnNum := 10;
BEGIN
OPEN c_1;
loop
FETCH c_1 into myRow
exit when c_1%notFOUND
exit when i>=returnNum;
update QueueTest set Locked='myLock' where id=myrow.id and locked is null;
i := i + sql%rowcount;
END
CLOSE c_1;
commit;
END;
我是用记事本写的,所以可能有问题,您可以将其修改为程序或其他。
【讨论】:
【参考方案6】:首先感谢前 2 个答案..从他们那里学到了很多东西。我测试了以下代码,在运行 Practicedontdel.java main 方法后,我发现这两个类每次都打印不同的行。如果在任何情况下此代码可能会失败,请告诉我。(PS:感谢堆栈溢出)
Practicedontdel.java:
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs =null;
String val="";
int count =0;
conn = getOracleConnection();
conn.setAutoCommit(false);
ps = prepareStatement(conn,"SELECT /*+FIRST_ROWS_3*/ t.* from
REPROCESS_QUEUE t FOR UPDATE SKIP LOCKED");
ps.setFetchSize(3);
boolean rss = ps.execute();
rs = ps.getResultSet();
new Practisethread().start();
while(count<3 && rs.next())
val = rs.getString(1);
System.out.println(val);
count++;
Thread.sleep(10000);
conn.commit();
System.out.println("end of main program");
Practisethread.java:在run()中:
conn = getOracleConnection();
conn.setAutoCommit(false);
ps = prepareStatement(conn,"SELECT /*+FIRST_ROWS_3*/ t.* from REPROCESS_QUEUE t FOR UPDATE SKIP LOCKED");
ps.setFetchSize(3);
boolean rss = ps.execute();
rs = ps.getResultSet();
while(count<3 && rs.next())
val = rs.getString(1);
System.out.println("******thread******");
System.out.println(val);
count++;
Thread.sleep(5000);
conn.commit();
System.out.println("end of thread program");
【讨论】:
以上是关于强制 Oracle 返回带有 SKIP LOCKED 的 TOP N 行的主要内容,如果未能解决你的问题,请参考以下文章
我的 FreeMarker 方法返回一个带有 $variable 的字符串——如何强制 FreeMarker 解析它?
httprunner学习19-跳过用例skip/skipIf/skipUnless