在数据库表中同时读取和更新

时间:2013-02-27 15:36:51

标签: c# oracle entity-framework transactions devart

我有一个使用Devart和Entity Framework访问的Oracle数据库。

有一个名为IMPORTJOBS的表格,其中包含STATUS列。

我还有多个进程同时运行。他们每个人都会在IMPORTJOBS中读取状态为'REGISTERED'的第一行,将其置于状态'EXECUTING',如果已完成,则将其置于状态'EXECUTED'

现在因为这些进程并行运行,我相信可能会发生以下情况:

  • 进程A读取具有状态REGISTERED
  • 的第10行
  • 流程B还会读取仍处于状态REGISTERED的行10,
  • 进程A将第10行更新为状态EXECUTING

进程B不应该读取第10行,因为进程A已经读取了它并且将更新其状态。

我该如何解决这个问题?在事务中放入读取和更新?或者我应该使用一些版本控制方法或其他什么?

谢谢!

编辑:感谢接受的答案,我让它工作并在此处记录:http://ludwigstuyck.wordpress.com/2013/02/28/concurrent-reading-and-writing-in-an-oracle-database

3 个答案:

答案 0 :(得分:2)

每个进程都可以发出SELECT ... FOR UPDATE来在读取时锁定该行。在这种情况下,进程A将读取并锁定该行,进程B将尝试读取该行并阻塞,直到进程A通过提交(或回滚)其事务来释放锁。然后,Oracle将确定该行是否仍符合B的标准,并且在您的示例中,不会将该行返回到B.这可以,但这意味着您的多线程进程现在可以实际上是单线程的,具体取决于您的事务控制方式需要工作。

提高可扩展性的可能方法

  • 消费者解决此问题的一种相对常见的方法是让一个协调器线程从表中读取数据,将工作分配到不同的线程,并适当地更新表(包括知道如何重新分配作业)如果分配给它的线程已经死亡。)
  • 如果您使用的是Oracle 11.1或更高版本,则可以使用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保留其行,会发生什么?这将如何影响您的方案:

  1. 会话A获得10行未处理。
  2. 会话B将获得相同的10行,被阻止并等待会话A.
  3. 会话A更新所选行的状态并提交其事务。
  4. Oracle现在(自动)从头开始重新运行会话B的选择,因为数据已被修改,我们已指定FOR UPDATE(此子句强制Oracle获取块的最后一个版本)。
    这意味着会话B将获得10个新行
  5. 因此,在这种情况下,您没有一致性问题。此外,假设请求行并更改其状态的事务很快,并发影响将很小。

答案 2 :(得分:1)

使用versioning and optimistic concurrency

IMPORTJOBS表格应该有一个时间戳列,您在模型中标记为ConcurrencyMode = Fixed。现在,当EF尝试进行更新时,时间戳列包含在更新语句中:WHERE timestamp = xxxxx

对于B,时间戳在平均时间内发生了变化,因此引发了并发异常,在这种情况下,您可以通过跳过更新来处理。

我来自SQL服务器后台,我不知道Oracle等价的时间戳(或rowversion),但我们的想法是,它是一个在对记录进行更新时自动更新的字段。