价格重新编制索引导致结帐时数据库死锁


47

我遇到了一个问题,我认为产品价格重新索引编制过程在结帐过程中导致了死锁异常。

我在结帐过程中捕获了此异常:

订单转换异常:SQLSTATE [40001]:序列化失败:1213尝试获取锁时发现死锁;否则,错误代码为:尝试重新启动事务

不幸的是,由于捕获异常的原因,我没有完整的堆栈跟踪信息,但是通过检查INNODB状态,我能够找到死锁:

SELECT `si`.*, `p`.`type_id` FROM `cataloginventory_stock_item` AS `si` 
INNER JOIN `catalog_product_entity` AS `p` ON p.entity_id=si.product_id     
WHERE (stock_id=1) 
AND (product_id IN(47447, 56678)) FOR UPDATE

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 0 page no 329624 n bits 352 index 
`PRIMARY` of table `xxxx`.`catalog_product_entity` 

SQL请求表锁最终是从 Mage_CatalogInventory_Model_Stock::registerProductsSale()尝试获取当前清单计数以使其递减时生成的。

发生死锁时,“产品价格重新索引”过程正在运行,我假设它对catalog_product_entity table导致死锁的读取锁定。如果我正确地理解了死锁,那么任何读取锁都会导致死锁,但是产品价格重新索引会在相当长的时间内保持该锁,因为该站点有约50,000个产品。

不幸的是,此时,在结帐代码流中,已通过自定义付款模块向客户的信用卡收费,并且创建相应的订单对象失败。

我的问题是:

  • 定制支付模块逻辑是否有问题?也就是说,是否存在确保Magento在将付款委托给付款方式(信用卡)之前可以将报价免费转换为订单例外的流程?

编辑: 似乎支付模块逻辑确实有问题,因为对$ paymentmethod-> authorize()的调用应在发生此死锁的位置之后而不是之前发生(按照下面的Ivan的回答)。但是,交易仍将被僵局阻止(尽管不会向信用卡收取错误费用)。

  • 此函数调用$stockInfo = $this->_getResource()->getProductsStock($this, array_keys($qtys), true);Mage_CatalogInventory_Model_Stock::registerProductsSale()使其锁定读,这是多么危险将是使之成为非锁定读?

  • 在网络上搜索答案时,有两个地方建议在网站很热时不运行完全重新索引;似乎不是一个好的解决方案;导致表死锁和锁争用的索引问题是Magento中的一个已知问题,是否有解决方法?

编辑: 似乎这里剩下的问题是第三个问题中的一个;重新索引会导致表死锁。寻找解决方法。

编辑:死锁不是问题本身,而是问题的解决本身应该成为焦点,这一概念很有意义。进一步调查以找到代码中的点以捕获死锁异常并重新发出请求。在Zend Framework DB适配器级别执行此操作是一种方法,但我也在寻找一种方法来在Magento代码中执行此操作以简化可维护性。

此线程中有一个有趣的补丁:http : //www.magentocommerce.com/boards/viewthread/31666/P0/似乎可以解决相关的死锁条件(但不是专门解决此问题)。

编辑:显然,死锁已在CE 1.8 Alpha中得到了解决。仍在寻找解决方法,直到此版本退出Alpha


我们最近一直在与一个类似的问题作斗争,您使用的是哪种付款扩展名?
彼得·奥卡拉汉

这是一个自定义编码的扩展程序
Roscius,2013年

1
@kalenjordan 1.13中的索引改进和下面的philwinkle等重试方案对我而言已大大缓解了这个问题。
Roscius

1
@Roscius大概减轻了多少?我看到某种形式的数据库故障(连接超时,锁定等待超时,死锁)影响约0.2%的订单。非常少见,但我真的想完全解决它。
kalenjordan 2014年

Answers:


16

您的付款方式很有可能错误地处理了付款。

Magento订单保存过程非常简单:

  • 准备应该从报价项目转移到订单项目的所有数据,包括价格和产品信息,然后不调用价格检索。
  • 调用之前为了提交事件checkout_type_onepage_save_ordersales_model_service_quote_submit_before
    • Mage_CatalogInventory_Model_Stock::registerProductsSale() 在此事件观察者处调用
  • 开始数据库交易
  • 调用$order->place()方法,其处理通过调用付款$paymentMethod->authorize()$paymentMethod->capture()$paymentMethod->initialize()依赖于它的逻辑。
  • 调用$ order-> save()方法将已处理的订单保存到DB表中sales_flat_order_*
  • 提交数据库事务(在这一步,数据库释放对库存表的锁定)

如您所见,这种付款方式不可能在库存锁定和读取产品价格或产品信息之前收取费用。

仅在以下情况下才可能执行付款方式:在执行用于收费操作的API调用后,付款方式将商品本身加载到价格中。

希望这可以帮助您调试问题。

至于重新编制索引,如果您的付款方式没有此问题,那应该是安全的。由于依赖锁的读取操作是在收取费用之前执行的。


1
谢谢,看来自定义支付模块的逻辑有点差了。但是,看起来索引过程仍然会引起异常,从而阻止结帐registerProductsSale()(这是因为对自定义付款模块的修复将消除对客户卡收取费用的问题)。
Roscius

8

因为这是一个自定义扩展名,所以我们可以找到一个自定义解决方法(阅读:hack),以在不编辑核心文件的情况下重试保存。

通过将以下两种方法添加到帮助器类中,我解决了所有的僵局困境。$product->save()现在不打电话,而是打电话Mage::helper('mymodule')->saferSave($product)

/**
 * Save with a queued retry upon deadlock, set isolation level
 * @param  stdClass $obj object must have a pre-defined save() method
 * @return n/a      
 */
public function saferSave($obj)
{

    // Deadlock Workaround
    $adapter = Mage::getModel('core/resource')->getConnection('core_write');
    // Commit any existing transactions (use with caution!)
    if ($adapter->getTransactionLevel > 0) {
        $adapter->commit();
    }
    $adapter->query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');

    //begin a retry loop that will recycle should a deadlock pop up
    $tries = 0;
        do {
            $retry = false;
            try {
                $obj->save();
            } catch (Exception $e) {
                if ($tries < 10 and $e->getMessage()=='SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction') {
                    $retry = true;
                } else {
                    //we tried at least 10 times, go ahead and throw exception
                    throw new Zend_Db_Statement_Exception($e->getMessage());
                }
                sleep($this->getDelay());
                $tries++;
            }
        } while ($retry);

    //free resources
    unset($adapter);
    return;
}

public function getDelay($tries){
    return (int) pow(2, $tries);
}

这完成了两个不同的任务-遇到死锁时,它使重试排队,并为该重试设置成倍增加的超时。它还设置事务隔离级别。有关SO和DBA.SE的信息很多,以获取有关MySQL事务隔离级别的更多信息。

FWIW,此后我再也没有遇到过僵局。


1
@Mage :: getModel('core / resource')@应该创建一个新的连接。我不了解它如何改变当前的事务隔离级别。
giftnuss13年

@giftnuss足够公平。一定要单身。随意在github上的死锁模块中贡献它
philwinkle

@philwinkle感谢这个人。我正在尝试确定EE 1.13升级是否可以解决我的难题,或者我是否也应该对此进行研究。我知道1.13确实可以异步建立索引,但是如果涉及相同的基础查询,我很难理解单独使用异步如何防止死锁的发生。
kalenjordan 2014年

1
@kalenjordan它是异步和1.8 / 1.13中更新的varien db变化的组合,可减少死锁的可能性。
philwinkle 2014年

我认为您忘了$tries使用此功能sleep($this->getDelay());
塔希尔·亚辛

3

在Magento论坛上,他们谈论编辑Zend库文件:lib / Zend / Db / Statement / Pdo.php

原始的_execute函数:

public function _execute(array $params = null)
    {
        // begin changes
        $tries = 0;
        do {
            $retry = false;
            try {
                if ($params !== null) {
                    return $this->_stmt->execute($params);
                } else {
                    return $this->_stmt->execute();
                }
            } catch (PDOException $e) {
                #require_once 'Zend/Db/Statement/Exception.php';
                if ($tries < 10 and $e->getMessage()=='SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction') {
                    $retry = true;
                } else {
                    throw new Zend_Db_Statement_Exception($e->getMessage());
                }
                $tries++;
            }
        } while ($retry);
        // end changes
    }

修改后:

public function _execute(array $params = null)
    {
        $tries = 0;
        do {
            $retry = false;
            try {
                $this->clear_result();
                $result = $this->getConnection()->query($sql);
                $this->clear_result();
            }
            catch (Exception $e) {
                if ($tries < 10 and $e->getMessage()=='SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction') {
                    $retry = true;
                } else {
                    throw $e;
                }
                $tries++;
            }
        } while ($retry);

        return $result;
    }

如您所见,唯一更改的是$ tries已移出循环。

与往常一样,建议在开发/测试环境中尝试此操作,而不是立即在生产环境中部署此修补程序。


2
我担心编辑底层框架文件,似乎重试应该在Magento代码级别进行。
Roscius

我们已经尝试了建议的修复程序,并且确实确实防止了此特定的死锁引起问题。我们还收到了对sales_flat_order_grid的保存的僵局,有了此修复程序,他们反而抛出了完整性约束违规,这显然不好。
彼得·奥卡拉汉

2

我在Magento 1.11网站上也遇到了同样的问题,并且自2012年11月12日以来就拥有Magento的公开票。他们确认这是一个问题,并假设正在创建补丁。

我的问题是,为什么此时必须重新编制价格索引?我认为这不是必需的:

#8 /var/www/html/app/code/core/Mage/CatalogInventory/Model/Observer.php(689): Mage_Catalog_Model_Resource_Product_Indexer_Price->reindexProductIds(Array)

1
如果某个产品缺货并且不应在目录中显示缺货产品,那么我相信它们的优点在于没有价格指数记录,最终将价格加入其中,最终将它们从产品集合中排除。
davidalger

这不能回答问题。您似乎正在尝试向原始问题添加其他信息。也许将这些信息作为对原始问题的评论会更好。
卢克·米尔斯

我和你在一起,金。自11/2011起,我有同样的门票开放。
philwinkle

我知道从技术上讲这不是一个答案,而是一个子问题,但是它确实回答了将该问题引用为重复的问题!因此,金伯利·托马斯(Kimberly Thomas)和大卫·戴维格(davidalger)回答了我的具体问题“为什么要重新建立价格索引?”,我对此表示赞同。我现在正在谷歌搜索的问题!谢谢!
天鹅绒数字

0

当在重新索引期间进行某些调用时,我们遇到了类似的死锁问题。对我们而言,它主要是在客户向购物车中添加商品时体现出来的。尽管可能无法解决实际的根本问题,但实现异步重新索引已完全停止了我们之前看到的所有死锁调用。在解决根本问题并将其推向EE / CE版本之前,它应该是一个权宜之计(我们最终为此购买了扩展程序)。


0

我建议您安装Philwinkle DeadlockRetry。它适用于我们的数据库。

https://github.com/philwinkle/Philwinkle_DeadlockRetry

我还建议您查看影响您的网络api的任何外部程序。我们有一个正在更新产品的数量并导致许多僵局。我们重新编写了代码,然后直接进入数据库。


1
该存储库不再受支持,但幸运的是,它建议替换它github.com/AOEpeople/Aoe_DbRetry

-1

去年,我多次遇到死锁问题,我只是通过增加服务器的内存来解决此问题,因为索引过程会占用所有资源。

您还应该使用我们使用miravist的异步重新索引解决方案

为了使系统更稳定,您应该考虑将后端与前端分开,以免它们占用彼此的RAM。

以我的经验,这不是源代码的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.