解决 Magento ORM 竞争条件

Solving Magento ORM race condition

作为 magento 专业服务和模块开发人员,我们需要实施 Magento 订单处理工作流,其中从 2 个独立的并发进程(第 3 方支付处理通知处理程序和客户到订单分配处理程序)加载、更新和存储订单信息。目前的实施工作正常,除了该过程容易受到竞争条件问题的影响。

进程 A: load()ᴬ -> updateFields1() -> save()ᴬ

进程 B: load()ᴮ -> updateFields2() -> save()ᴮ

如果在 load()ᴮ 之后但在 save()ᴮ 之前调用 load()ᴬ,其中一个进程会覆盖并发进程设置的值。

Magento 框架是否有可能或提供一些常见做法来处理像 updateField1() 和 updateField2() 方法中更新的字段完全不同的竞争条件?

基本上有两个选项可以避免这种竞争情况。他们都有取舍。

选项 A: 进行两个单独的 UPDATE 查询来更新这些字段,并让您的数据库服务器为您处理锁定。 Zend 的 DB Framework 已经具有用于进行此类更新的内置方法。这里有两个示例函数可以帮助您入门:

function paymentProcessingUpdate($incrementId, $paymentId) {
    $db = Mage::getSingleton('core/resource')->getConnection('core_write');
    $table = 'sales_flat_order';
    $data = array('payment_id' => $paymentId);
    $where = $db->quoteInto('increment_id = ?', $incrementId);
    $db->update($table, $data, $where);
}

function assignCustomerToOrder($incrementId, $customerId) {
    $db = Mage::getSingleton('core/resource')->getConnection('core_write');
    $table = 'sales_flat_order';
    $data = array('customer_id' => $customerId);
    $where = $db->quoteInto('increment_id = ?', $incrementId);
    $db->update($table, $data, $where);
}

这种方法的缺点:

  1. 如果您的字段在 sales_flat_order_grid table 中使用,您也必须更新 table。这是 Magento 的模型通常会为您做的事情。您可以添加一个 JOIN 来在单个 UPDATE 查询中处理此问题。
  2. 它绕过了 Magento 模型,这意味着像 sales_order_save_commit_aftersales_order_save_after 这样的观察者不会被触发。

优点:

  1. 运行 单个更新比保存整个模型要快得多。

选项 B: 您可以使用像 Mage_Index_Model_Lock 这样的锁定机制,这是 Magento 在刷新索引时所做的。

工作原理:假设我们正在处理订单号 100002185。在此示例中,进程 A 和 B 可以互换。

  1. 进程 A 启动并检查是否有 automation_order_id_100002185.
  2. 的锁
  3. 进程 A 没有找到锁,因此它为 automation_order_id_100002185 设置了一个锁并开始工作。
  4. 进程 B 在进程 A 完成之前启动,并检查 automation_order_id_100002185 的锁。
  5. 进程B找到一个锁,所以它休眠了3秒。
  6. 进程A仍在运行。进程 B 在休眠 3 秒后再次检查锁,但是进程 A 还没有释放它,所以它又休眠了 3 秒。
  7. 进程 A 完成其工作,并释放锁。
  8. 进程 B 在其 3 秒休眠后再次检查锁。这次没有锁,所以进程B设置了一个锁,开始做它的工作。
  9. 进程 B 完成其工作并释放锁。

这种方法的缺点:

  1. 它牺牲了速度。如果每个进程更新的字段不同、不相关且独立,则它们可以并行工作。
  2. 灾难性的服务器故障(或重启)可能导致锁永远不会被释放。
  3. 如果永远不释放锁,进程可能会无限期地休眠,并且它的工作可能永远无法完成。这可能会导致进程堆积并最终导致服务器崩溃,从而对其他进程造成连锁反应。
  4. 如果服务器崩溃,并且您没有某种队列可以依靠,您的处理可能永远不会真正完成。 Magento 2 使用 RabbitMQ 来解决这个问题,但您可以使用数据库 table 和 cron 脚本来实现类似的机制。