我需要您提供一个想法来解决我的小问题。我有一个表,其中包含一些作业,这些作业具有ID,TIMESTAMP,STATE和一些其他值,这些值定义作业的实际含义。我需要找到具有最低TIMESTAMP的ID,其中STATE = 1,并且必须在同一原子操作中将STATE设置为2。
如果实际上只有一个客户端连接到数据库,这将是这样做的方法: 首先选择时间戳最少的ID。
SELECT * FROM SW_ASYNC_JOBS WHERE STATE = 1 ORDER BY TIMESTAMP FETCH FIRST 1 ROW ONLY
我们将ID存储到一个变量中,例如JOB_ID,然后将其STATE设置为2:
UPDATE SW_ASYNC_JOBS SET STATE = 2 WHERE JOB_ID = :JOB_ID
现在,客户端拥有了所需的所有数据,将状态设置为“进行中”并开始处理工作。
当然,实际上在这两个操作之间会有另一个客户端执行相同的操作,并且肯定会出现竞争状况。两位客户可能会从事致命的同一工作。
我在网上搜索时发现SELECT FOR UPDATE和WHERE CURRENT OF语句,但是似乎无法同时检索作业的所有列。我从这样的事情开始:
DECLARE
CURSOR FRESH_JOB IS
SELECT * FROM SW_ASYNC_JOBS WHERE STATE = 1 ORDER BY TIMESTAMP FETCH FIRST 1 ROW ONLY
FOR UPDATE;
BEGIN
FOR JOB IN FRESH_JOB LOOP
UPDATE SW_ASYNC_JOBS SET STATE = 2 WHERE CURRENT OF FRESH_JOB;
END LOOP;
END;
但是由于某种原因,我得到了一个错误ORA-02014: cannot select FOR UPDATE from view
,因为表SW_ASYNC_JOBS是一个简单的表,主键超过两列。
解决该问题的最佳方法是什么?我应该锁定整个表以获取最旧的作业并更改其状态吗?
为完整起见,这是我正在谈论的表格:
CREATE TABLE SW_ASYNC_JOBS (
"MASTER_JOB_ID" NUMBER(22, 0) NOT NULL,
"JOB_ID" NUMBER(22, 0) NOT NULL,
"USER_ID" VARCHAR2(64) NOT NULL,
"UID" VARCHAR2(64) NOT NULL,
"VIEW" VARCHAR2(64) NOT NULL,
"JSON" VARCHAR2(2000) NOT NULL,
"TIMESTAMP" TIMESTAMP NOT NULL,
"STATE" NUMBER(2, 0) NOT NULL,
CONSTRAINT "SW_ASYNC_PRIMARY" PRIMARY KEY ("MASTER_JOB_ID", "JOB_ID")
)
还有其他客户端从一个序列中检索新的序列号,以向该表中添加新行。首先,检索MASTER_JOB_ID,然后为每个“从属作业”使用另一个新的序列号。因此,原则上MASTER_JOB_ID
和JOB_ID
中的数字不能出现两次。 MASTER_JOB_ID
仅用于组合多个“从属作业”并明智地显示其状态组。
客户端是一个Python脚本,它使用版本12.1.0.2.0中的cx_Oracle
包。
答案 0 :(得分:0)
也许我不明白这个问题,但是-为什么将SELECT
和UPDATE
分开?这样的声明不会完成这项工作吗? RETURNING
子句用于返回JOB_ID。
declare
retval sw_async_jobs.job_id%type;
begin
update sw_async_jobs s set
s.state = 2
where s.job_id = (select s1.job_id
from sw_async_jobs s1
where s1.state = 1
order by s1.timestamp fetch first 1 row only
)
returning job_id into retval;
dbms_output.put_line('retval = ' || retval);
end;
/
答案 1 :(得分:0)
我想我为此找到了另一个也许非常规的解决方案。
def getNextJobId(self) :
""" Return Id of the oldest job which is in 'ready' state
"""
# Lock the table
cursor = self._execute("LOCK TABLE %s IN EXCLUSIVE MODE" % self._jobTableName)
try :
jobId = None
while jobId is None :
# Find potential job Id in 'ready' state
cursor = self._execute("SELECT JOB_ID FROM %s WHERE STATE = :READY_STATE ORDER BY TIMESTAMP, JOB_ID FETCH FIRST 1 ROW ONLY" % self._jobTableName,
{'READY_STATE' : JobStates.ready.value})
rows = list(cursor)
# Is there is no such job, quit
if len(rows) == 0 :
self._rollback()
break
# If there is a potential job, save its Id
elif len(rows) >= 1 :
# 'rows' has this form: [(int,)]
potentialJobId = rows[0][0]
# Now we change its state. In the WHERE clause we also check if the job still is in 'ready' state.
statement = "UPDATE %s SET STATE = :WORKING_STATE WHERE JOB_ID = :JOB_ID AND STATE = :READY_STATE" % self._jobTableName
cursor = self._execute(statement, {'READY_STATE' : JobStates.ready.value,
'WORKING_STATE' : JobStates.working.value,
'JOB_ID' : potentialJobId})
# If there was exactly one row changed, we now have the job
if cursor.rowcount == 1 :
jobId = potentialJobId
self._commit()
# If nothing was changed through the UPDATE, another client got the same potential job and changed the state already. Try again.
return jobId
finally :
self._rollback()
由于表已锁定并且UPDATE
是原子的,因此现在应该可以正常工作了。起初,我有兴趣避免循环,但是当我在Freenode上与#oracle-database
中的一个人聊天时,看起来当以其他方式尝试并且没有循环时,就会出现竞争情况。
我不想将此解决方案标记为该问题的解决方案。也许其他人有一个更好的主意,可以使用Python和cx_Oracle。