有一个few questions关于如何在Oracle和SQL Server中实现类似队列的表(锁定特定行,选择一定数量的行,以及跳过当前锁定的行)。 / p>
假设至少N
行符合条件,我如何保证检索到一定数量(N
)行?
从我所看到的,Oracle在确定要跳过的行之前应用WHERE
谓词。这意味着如果我想从表中拉出一行,并且两个线程同时执行相同的SQL,则一个将接收该行,另一个将接收一个空结果集(即使有更多符合条件的行)。
这与SQL Server似乎处理UPDLOCK
,ROWLOCK
和READPAST
锁定提示的方式相反。在SQL Server中,TOP
在成功获得锁定后,神奇地似乎限制了的记录数。
ORACLE
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
请注意,第一个返回一行,第二个会话不返回一行:
第一节
ID ---- 4
第二节
ID ----
SQL SERVER
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;
请注意,两个会话都返回不同的行。
第一节
ID ---- 4
第二节
ID ---- 3
如何在Oracle中获得类似的行为?
答案 0 :(得分:14)
“从我所看到的,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;
答案 1 :(得分:10)
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;
根据以下评论,建议使用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
现在,为了最终达到目的 - 您可以将更新跳过锁定在正确的位置吗?
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 update移动到视图中会出现语法错误:
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将给出零行,这真的是错误的!
那你能做什么?打开游标并从中获取所需的行数:
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中再次运行它,它会在跳过第1行时打印“2”并获得下一个免费的。
这个例子在PLSQL中,但是在Java中使用setFetchSize你应该能够得到完全相同的行为。
答案 2 :(得分: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
你的内部选择尝试只抓取id = 4并锁定它。这是成功的,因为此单行尚未锁定。
在第二个会话中,您的内部选择STILL尝试抓住 ONLY id = 4 并锁定它。这不成功,因为第一个会话仍然锁定了该行。
现在,如果您在第一个会话中更新了“已锁定”字段,则运行该选择的下一个会话将获取id = 3.
基本上,在您的示例中,您依赖于未设置的标志。要使用锁定的标志,您可能需要执行以下操作:
然后,您可以使用select for update skip locked语句,因为您正在维护锁定的标志。
就个人而言,我不喜欢所有标志的更新(你的解决方案可能因任何原因需要它们),所以我可能只是尝试来选择我想要更新的ID(通过每个会话中的任何标准:
select * from queuetest where ... for update skip locked;
例如(实际上,我的标准不是基于id列表,但是queuetest表过于简单化):
sess 1:select * from queuetest where (4,3)中的id用于更新已跳过锁定;
sess 2:select * from queuetest where id(4,3,2)中的更新已跳过锁定;
这里sess1会锁定4,3而sess2只会锁定2。
你不能在我的知识中做一个top -n或在select for update语句中使用group_by / order_by等,你会得到一个ORA-02014。
答案 3 :(得分:1)
我的解决方案 - 就是编写这样的存储过程:
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行 - 将单个fetch替换为局部变量(“i”),并将循环提取替换为临时表。
PS:返回光标 - 用于休眠友谊。
答案 4 :(得分:1)
我遇到了这个问题,我们花了很多时间解决它。某些使用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;
我在记事本中写了它,所以可能出了点问题,您可以将其作为一个过程进行修改,否则。
答案 5 :(得分:0)
首先感谢您对前2个问题的回答。从中学到很多。我测试了以下代码,并在运行Practicedontdel.java main方法后发现,这两个类每次都打印不同的行。请让我知道该代码在任何情况下都可能失败。(P.S:由于堆栈溢出)
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");