【问题标题】:Concurrent reading and updating in a database table数据库表中的并发读取和更新
【发布时间】:2013-02-13 11:50:22
【问题描述】:

我有一个使用 Devart 和实体框架访问的 Oracle 数据库。

有一个名为IMPORTJOBS 的表,其中有一列STATUS

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

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

  • 进程A读取状态为REGISTERED的第10行,
  • 进程 B 还读取第 10 行,其状态仍为 REGISTERED
  • 进程 A 将第 10 行更新为状态 EXECUTING

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

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

谢谢!

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

【问题讨论】:

    标签: c# oracle entity-framework transactions devart


    【解决方案1】:

    您应该使用数据库的内置锁定机制。不要重新发明轮子,特别是因为 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 行(如果可能)未锁定。这些行将被锁定,会话不会相互阻塞。

    在 10g 及之前,由于 Oracle 返回一致的结果,请明智地使用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 个新行

    所以在这种情况下,您没有一致性问题。另外,假设请求一行并改变其状态的事务很快,并发影响会很小。

    【讨论】:

    • 谢谢,我正在尝试执行“SELECT * FROM IMPORTJOBS WHERE STATUSCODE = 'REGISTERED' AND ROWNUM
    • (1) 确保您已关闭自动提交:您无法在没有事务的情况下锁定行。 (2) FOR UPDATE SKIP LOCKEDrownum won't work as you expect -- 这是因为 SKIP LOCKED 是在 WHERE 子句之后评估的。使用不带 rownum 的 select,获取一个(或多个根据需要)行并关闭游标,这是使用 SKIP LOCKED 的最佳方式。
    • 确实,我不得不将选择和更新放在一个事务中,现在它可以工作了。谢谢!!!
    【解决方案2】:

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

    提高可扩展性的可能方法

    • 消费者解决此问题的一种相对常见的方法是使用单个协调器线程从表中读取数据,将工作分配给不同的线程,并适当地更新表(包括知道如何重新分配作业如果分配给它的线程已经死亡)。
    • 如果您使用的是 Oracle 11.1 或更高版本,您可以在您的 FOR UPDATE 上使用 SKIP LOCKED clause,以便每个会话返回满足其条件且未被锁定的第一行(该子句在早期版本中存在,但未记录,因此它可能无法正常工作)。
    • 您可以使用具有多个消费者的队列,而不是为ImportJobs 使用表。这将允许 Oracle 将消息分发到每个进程,而无需构建任何额外的锁定(Oracle 队列在幕后完成这一切)。

    【讨论】:

      【解决方案3】:

      使用versioning and optimistic concurrency

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

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

      我来自 SQL 服务器背景,我不知道时间戳(或行版本)的 Oracle 等效项,但我的想法是它是一个在对记录进行更新时自动更新的字段。

      【讨论】:

        猜你喜欢
        • 2017-11-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-15
        • 2017-10-19
        • 2018-12-28
        • 1970-01-01
        相关资源
        最近更新 更多