我有一个使用Devart和Entity Framework访问的Oracle数据库。
有一个名为IMPORTJOBS
的表格,其中包含STATUS
列。
我还有多个进程同时运行。他们每个人都会在IMPORTJOBS
中读取状态为'REGISTERED'
的第一行,将其置于状态'EXECUTING'
,如果已完成,则将其置于状态'EXECUTED'
。
现在因为这些进程并行运行,我相信可能会发生以下情况:
REGISTERED
,REGISTERED
的行10,EXECUTING
。进程B不应该读取第10行,因为进程A已经读取了它并且将更新其状态。
我该如何解决这个问题?在事务中放入读取和更新?或者我应该使用一些版本控制方法或其他什么?
谢谢!
编辑:感谢接受的答案,我让它工作并在此处记录:http://ludwigstuyck.wordpress.com/2013/02/28/concurrent-reading-and-writing-in-an-oracle-database。
答案 0 :(得分:2)
每个进程都可以发出SELECT ... FOR UPDATE
来在读取时锁定该行。在这种情况下,进程A将读取并锁定该行,进程B将尝试读取该行并阻塞,直到进程A通过提交(或回滚)其事务来释放锁。然后,Oracle将确定该行是否仍符合B的标准,并且在您的示例中,不会将该行返回到B.这可以,但这意味着您的多线程进程现在可以实际上是单线程的,具体取决于您的事务控制方式需要工作。
提高可扩展性的可能方法
FOR UPDATE
上的SKIP LOCKED
clause,以便每个会话都返回符合其条件且未锁定的第一行(该子句存在于早期版本,但未记录,因此可能无法正常工作)。ImportJobs
表。这将允许Oracle将消息分发到每个进程,而无需构建任何额外的锁定(Oracle队列在幕后完成所有操作)。答案 1 :(得分:2)
您应该使用数据库的内置锁定机制。不要重新发明轮子,特别是因为RDBMS 设计来处理并发性和一致性。
在Oracle 11g中,我建议您使用SKIP LOCKED
功能。例如,每个进程都可以调用这样的函数(假设id
是数字):
CREATE OR REPLACE TYPE tab_number IS TABLE OF NUMBER;
CREATE OR REPLACE FUNCTION reserve_jobs RETURN tab_number IS
CURSOR c IS
SELECT id FROM IMPORTJOBS WHERE STATUS = 'REGISTERED'
FOR UPDATE SKIP LOCKED;
l_result tab_number := tab_number();
l_id number;
BEGIN
OPEN c;
FOR i IN 1..10 LOOP
FETCH c INTO l_id;
EXIT WHEN c%NOTFOUND;
l_result.extend;
l_result(l_result.size) := l_id;
END LOOP;
CLOSE c;
RETURN l_result;
END;
这将返回未锁定的10行(如果可能)。这些行将被锁定,会话不会相互阻塞。
在Oracle中返回一致结果之前的10g和之前,明智地使用FOR UPDATE
并且您不应该遇到您描述的问题。例如,请考虑以下SELECT
:
SELECT *
FROM IMPORTJOBS
WHERE STATUS = 'REGISTERED'
AND rownum <= 10
FOR UPDATE;
如果所有进程都使用此SELECT保留其行,会发生什么?这将如何影响您的方案:
FOR UPDATE
(此子句强制Oracle获取块的最后一个版本)。因此,在这种情况下,您没有一致性问题。此外,假设请求行并更改其状态的事务很快,并发影响将很小。
答案 2 :(得分:1)
使用versioning and optimistic concurrency。
IMPORTJOBS
表格应该有一个时间戳列,您在模型中标记为ConcurrencyMode
= Fixed
。现在,当EF尝试进行更新时,时间戳列包含在更新语句中:WHERE timestamp = xxxxx
。
对于B
,时间戳在平均时间内发生了变化,因此引发了并发异常,在这种情况下,您可以通过跳过更新来处理。
我来自SQL服务器后台,我不知道Oracle等价的时间戳(或rowversion),但我们的想法是,它是一个在对记录进行更新时自动更新的字段。