天天看點

記幾個常見的Laravel報錯

我已經用了一段時間的 Laravel 架構了,期間遇到了不少問題,有一些調試起來着實不太容易,本文篩選出幾個,如果能讓大家少走一些彎路,那我就算沒白寫。

報錯:「Can’t swap PDO instance while within transaction」

通過查詢 Laravel 源代碼,可以确認異常是在 setPdo 方法中抛出的:

<?php

public function setPdo($pdo)
{
    if ($this->transactions >= 1) {
        throw new RuntimeException("
            Can't swap PDO instance while within transaction.
        ");
    }

    $this->pdo = $pdo;

    return $this;
}

?>           

複制

按字面意思了解,出現此錯誤是因為在開啟了事務的情況下,切換了資料庫連接配接。不過有時候,即便代碼裡沒有顯式的切換資料庫連接配接,也有可能出現此錯誤。比如說在執行查詢語句出錯的時候,系統會通過 tryAgainIfCausedByLostConnection 方法判斷問題是不是因為丢失連接配接導緻的,如果是,那麼系統會通過 reconnect 方法重新連接配接,在重新連接配接的時候,系統會通過 disconnect 方法執行一些清理工作,其中調用了 setPdo 方法。

理清了前因後果,自然就知道如何解決問題了:檢查網絡情況,确認資料庫連接配接丢失的原因,這可能是某個裝置有問題,也可能是某個 timeout 設定不當所緻。一個相對 dirty 的處理方法是在查詢前執行一下 DB::reconnect() 方法重新連接配接一下資料庫。

報錯:「Cannot delete job <ID>: NOT_FOUND」

此問題實際上和 Laravel 沒太大關系,而是隊列服務 Beanstalk 導緻的。

記幾個常見的Laravel報錯

Beanstalk

要解決這個問題,需要先了解一個消息的生命周期:當一個消息被放入隊列的時候,它就進入了 READY 狀态,與此同時,它會關聯一個 TTR(time to run) 計時器,表示此消息允許運作的時間,當此消息被消費時,它就進入了 RESERVED 狀态,消費完後,此消息就會被删除,如果消費的時間過長,比 TTR 還長,那麼系統會認為認為此消費者已經挂了,進而會把消息從 RESERVED 狀态退回到 READY 狀态,交給另一個消費者重新處理。于是乎同一個消息可能會被多個消費者處理,第一個處理完的消費者可以正常的删除消息,而其餘的消費者在删除消息的時候就會報無法删除的錯誤。

解決方法很簡單,首先,需要確定 TTR 的設定不能太小;其次,實際上 Beanstalk 提供了一個專門的 touch 指令來解決執行時間過長的問題,此外,有些時候我們可能需要在應用層面上通過加鎖來規避同一個消息被多個消費者同時處理的情況。

報錯:「No query results for model」

在激活了 Laravel 讀寫分離的前提下,當消費者處理消息的時候,可能會收到類似錯誤。一個有潛在問題的隊列指令大概如下所示:

<?php

class Foo extends Command implements SelfHandling, ShouldBeQueued
{
    use InteractsWithQueue, SerializesModels;

    protected $bar;

    public function __construct($id)
    {
        $this->bar = Bar::find($id);
    }

    public function handle()
    {
        // $this->bar
    }
}

?>           

複制

很明顯,當開啟了 Laravel 讀寫分離的時候,因為主從延遲的緣故,是以 find 可能查詢不到相應的資料,一旦我們分析到了這裡,那麼很可能會把寫法修改成下面的樣子:

<?php

class Foo extends Command implements SelfHandling, ShouldBeQueued
{
    use InteractsWithQueue, SerializesModels;

    protected $bar;

    public function __construct($id)
    {
        $this->bar = Bar::onWriteConnection()->find($id);
    }

    public function handle()
    {
        // $this->bar
    }
}

?>           

複制

也就是說,通過 Laravel 的 onWriteConnection 方法把查詢固定在主伺服器上,不過實際上無效。問題症結在于反序列化的時候,系統會在從伺服器上一次 findOrFail 調用。

<?php

protected function getRestoredPropertyValue($value)
{
    return $value instanceof ModelIdentifier
        ? (new $value->class)->findOrFail($value->id) : $value;
}

?>           

複制

因為我們無法 HACK 到架構内部,是以 onWriteConnection 就沒有意義了。其實換個角度看問題,隻要在系列化的時候,保證别用資料庫對象做屬性即可:

<?php

class Foo extends Command implements SelfHandling, ShouldBeQueued
{
    use InteractsWithQueue, SerializesModels;

    protected $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function handle()
    {
        $bar = Bar::onWriteConnection()->find($this->id);
    }
}

?>           

複制

以上就是我遇到的幾個有代表性的報錯,歡迎大家一起交流。