解决 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);
}
这种方法的缺点:
- 如果您的字段在
sales_flat_order_grid
table 中使用,您也必须更新 table。这是 Magento 的模型通常会为您做的事情。您可以添加一个 JOIN
来在单个 UPDATE
查询中处理此问题。
- 它绕过了 Magento 模型,这意味着像
sales_order_save_commit_after
和 sales_order_save_after
这样的观察者不会被触发。
优点:
- 运行 单个更新比保存整个模型要快得多。
选项 B: 您可以使用像 Mage_Index_Model_Lock
这样的锁定机制,这是 Magento 在刷新索引时所做的。
工作原理:假设我们正在处理订单号 100002185
。在此示例中,进程 A 和 B 可以互换。
- 进程 A 启动并检查是否有
automation_order_id_100002185
. 的锁
- 进程 A 没有找到锁,因此它为
automation_order_id_100002185
设置了一个锁并开始工作。
- 进程 B 在进程 A 完成之前启动,并检查
automation_order_id_100002185
的锁。
- 进程B找到一个锁,所以它休眠了3秒。
- 进程A仍在运行。进程 B 在休眠 3 秒后再次检查锁,但是进程 A 还没有释放它,所以它又休眠了 3 秒。
- 进程 A 完成其工作,并释放锁。
- 进程 B 在其 3 秒休眠后再次检查锁。这次没有锁,所以进程B设置了一个锁,开始做它的工作。
- 进程 B 完成其工作并释放锁。
这种方法的缺点:
- 它牺牲了速度。如果每个进程更新的字段不同、不相关且独立,则它们可以并行工作。
- 灾难性的服务器故障(或重启)可能导致锁永远不会被释放。
- 如果永远不释放锁,进程可能会无限期地休眠,并且它的工作可能永远无法完成。这可能会导致进程堆积并最终导致服务器崩溃,从而对其他进程造成连锁反应。
- 如果服务器崩溃,并且您没有某种队列可以依靠,您的处理可能永远不会真正完成。 Magento 2 使用 RabbitMQ 来解决这个问题,但您可以使用数据库 table 和 cron 脚本来实现类似的机制。
作为 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);
}
这种方法的缺点:
- 如果您的字段在
sales_flat_order_grid
table 中使用,您也必须更新 table。这是 Magento 的模型通常会为您做的事情。您可以添加一个JOIN
来在单个UPDATE
查询中处理此问题。 - 它绕过了 Magento 模型,这意味着像
sales_order_save_commit_after
和sales_order_save_after
这样的观察者不会被触发。
优点:
- 运行 单个更新比保存整个模型要快得多。
选项 B: 您可以使用像 Mage_Index_Model_Lock
这样的锁定机制,这是 Magento 在刷新索引时所做的。
工作原理:假设我们正在处理订单号 100002185
。在此示例中,进程 A 和 B 可以互换。
- 进程 A 启动并检查是否有
automation_order_id_100002185
. 的锁
- 进程 A 没有找到锁,因此它为
automation_order_id_100002185
设置了一个锁并开始工作。 - 进程 B 在进程 A 完成之前启动,并检查
automation_order_id_100002185
的锁。 - 进程B找到一个锁,所以它休眠了3秒。
- 进程A仍在运行。进程 B 在休眠 3 秒后再次检查锁,但是进程 A 还没有释放它,所以它又休眠了 3 秒。
- 进程 A 完成其工作,并释放锁。
- 进程 B 在其 3 秒休眠后再次检查锁。这次没有锁,所以进程B设置了一个锁,开始做它的工作。
- 进程 B 完成其工作并释放锁。
这种方法的缺点:
- 它牺牲了速度。如果每个进程更新的字段不同、不相关且独立,则它们可以并行工作。
- 灾难性的服务器故障(或重启)可能导致锁永远不会被释放。
- 如果永远不释放锁,进程可能会无限期地休眠,并且它的工作可能永远无法完成。这可能会导致进程堆积并最终导致服务器崩溃,从而对其他进程造成连锁反应。
- 如果服务器崩溃,并且您没有某种队列可以依靠,您的处理可能永远不会真正完成。 Magento 2 使用 RabbitMQ 来解决这个问题,但您可以使用数据库 table 和 cron 脚本来实现类似的机制。