【问题标题】:Avoid background job to run simultaneously by two workers避免后台作业由两个工人同时运行
【发布时间】:2018-01-17 15:01:06
【问题描述】:

我有一个运行我们的网络服务请求的后台作业的守护进程。我们有 4 名工人同时运行。

有时一个作业会同时执行两次,因为两个工人决定运行该作业。为了避免这种情况,我们尝试了几件事:

  1. 由于我们的作业来自我们的数据库,我们添加了一个名为executed 的标志,以防止其他工作获取已经开始执行的作业;这并不能解决问题,有时我们数据库的延迟足以同时执行;
  2. 在系统中添加了memcached(所有工作人员都在同一个系统中运行),但不知何故,我们今天有同时运行的作业——memcached 也不能解决多台服务器的问题。

下面是我们目前使用的逻辑:

// We create our memcached server
$memcached = new Memcached();
$memcached->addServer("127.0.0.1", 11211);

// Checkup every 5 seconds for operations
while (true) {
    // Gather all operations TODO
    // In this query, we do not accept operations that are set
    // as executed already.
    $result = findDaemonOperationsPendingQuery();

    // We have some results!
    if (mysqli_num_rows($result) > 0) {
        $op = mysqli_fetch_assoc($result);
        echo "Found an operation todo #" . $op['id'] . "\n";

        // Set operation as executed
        setDaemonOperationAsDone($op['id'], 'executed');

        // Verifies if operation is happening on memcached
        if (get_memcached_operation($memcached, $op['id'])) {
            echo "\tOperation id already executing...\n";
            continue;

        } else {
            // Set operation on memcached
            set_memcached_operation($memcached, $op['id']);
        }

        ... do our stuff
    }
}

这类问题通常是如何解决的? 我在互联网上查找了一个名为 Gearman 的库,但我不相信当我们有多个服务器时它会解决我的问题。

我想到的另一件事是预定义一个守护程序以在插入时运行操作,并创建一个故障安全独占守护程序来运行由停止服务的守护程序设置的操作。

有什么想法吗?

谢谢。

【问题讨论】:

  • 将表锁定在“findDaemonOperationsPendingQuery”和“setDaemonOperationAsDone”之间可以解决我的问题吗?
  • 让我猜猜……你在使用 MyISAM?
  • 不,我正在使用 InnoDb

标签: php mysql concurrency daemon


【解决方案1】:

另一种使用锁和事务的解决方案,假设每个工作人员都有一个 id。

在你的循环运行中:

UPDATE operations SET worker_id = :wid WHERE worker_id IS NULL LIMIT 1;

SELECT * FROM operations where executed = 0 and worker_id = :wid;

更新是一个单一的原子操作,如果尚未设置,您只需设置 worker_id,因此无需担心竞争条件。设置 worker_id 可以明确谁拥有该操作。由于 LIMIT 1,更新只会分配一项操作。

【讨论】:

  • 聪明的主意!我会试试的。
  • 赞成,因为这是个好主意,但问题可能很快就会成为“如何确保 worker_id 的唯一性”:)
  • 取决于您如何运行工作程序,您可以连接主机名和进程 ID 或类似的东西。它很可能是一个字符串。
  • 你也可以在数据库中注册worker,那么id就是外键。
  • 我要做的是如下分配一个字符串:workerno_server_created_at (01_main_1516215036)
【解决方案2】:

你有一个典型的并发问题。

  1. 工人 1 读表,选择作业
  2. 工人 1 更新表以将作业标记为“已分配”或其他内容
  3. 等等,在 1 和 2 之间,工人 2 也读取了表格,由于该作业尚未标记为“已分配”,因此工人 2 选择了相同的作业

解决这个问题的方法是使用事务和锁,特别是SELECT.. FOR UPDATE。它会是这样的:

  1. Worker 1 启动事务 (START TRANSACTION) 并尝试获取排他锁 SELECT * FROM jobs [...] FOR UPDATE
  2. 工人 2 做同样的事情。除非他必须等待,因为 Worker 1 已经拥有锁。
  3. Worker 1 更新表,表示他现在正在处理该工作并立即提交事务。这释放了其他工作人员选择工作的锁定。工人 1 现在可以安全地开始这项工作了。
  4. Worker 2 现在可以读取表并获取锁。由于表格已更新,工人 2 将选择不同的工作。

编辑:关于您的 PHP 代码的具体评论:

  • 您的评论说您正在获取每个工作人员需要同时完成的所有工作。你应该只选择一个,做,选择一个,做,等等。
  • 您正在设置标志“已执行”,而实际上它尚未(尚未)执行。您需要一个“已分配”标志和一个不同的“已执行”标志。

【讨论】:

  • 这会阻止 Worker 2 在表中执行简单的SELECT,还是会专门阻止 Worker 2 启动事务并发出 SELECT [...] FOR UPDATE 命令?
  • @MaurícioGiordano 不,它不会阻止工人 2 做一个简单的SELECT。 Worker 2 必须尝试来获取锁,这就是锁定的重点。
  • 您不想在执行分配的任务时保持锁定,也不想将大量任务分配给工作人员然后执行它们,因此请确保您的 SELECT 限制为 1排。您将在循环的每次迭代中运行一个新事务和 SELECT 和 UPDATE,而不是对单个 SELECT 的行进行迭代。
  • @dhinchliff 确实我认为这很明显,因为我在回答中使用了“已分配”状态作为示例,但是再次查看问题中的 PHP 代码是对的,坚持下去是件好事关于这个...感谢您的评论。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-05-19
  • 1970-01-01
  • 2016-01-30
  • 1970-01-01
  • 1970-01-01
  • 2012-01-15
  • 1970-01-01
相关资源
最近更新 更多