在laravel

时间:2017-07-01 10:03:16

标签: mysql laravel eloquent

当我使用Laravel

执行一些插入/更新查询时,我的Laravel Eloquent应用程序出现此错误
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found

如何在完成查询之前重新执行查询?

3 个答案:

答案 0 :(得分:3)

此解决方案适用于Laravel 5.1,但我相信它可以用于框架的新版本,只需稍作修改。

以下代码假定默认数据库连接名称为“ mysql ”。请在config/database.php字段default中查看。

创建扩展Illuminate\Database\MySqlConnection的新班级:

namespace App\Helpers\MySQL;

use Closure;
use Exception;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\QueryException;
use Log;
use PDOException;

/**
 * Class DeadlockReadyMySqlConnection
 *
 * @package App\Helpers
 */
class DeadlockReadyMySqlConnection extends MySqlConnection
{
    /**
     * Error code of deadlock exception
     */
    const DEADLOCK_ERROR_CODE = 40001;

    /**
     * Number of attempts to retry
     */
    const ATTEMPTS_COUNT = 3;

    /**
     * Run a SQL statement.
     *
     * @param  string    $query
     * @param  array     $bindings
     * @param  \Closure  $callback
     * @return mixed
     *
     * @throws \Illuminate\Database\QueryException
     */
    protected function runQueryCallback($query, $bindings, Closure $callback)
    {
        $attempts_count = self::ATTEMPTS_COUNT;

        for ($attempt = 1; $attempt <= $attempts_count; $attempt++) {
            try {
                return $callback($this, $query, $bindings);
            } catch (Exception $e) {
                if (((int)$e->getCode() !== self::DEADLOCK_ERROR_CODE) || ($attempt >= $attempts_count)) {
                    throw new QueryException(
                        $query, $this->prepareBindings($bindings), $e
                    );
                } else {
                    $sql = str_replace_array('\?', $this->prepareBindings($bindings), $query);
                    Log::warning("Transaction has been restarted. Attempt {$attempt}/{$attempts_count}. SQL: {$sql}");
                }
            }
        }

    }
}

扩展基本连接工厂Illuminate\Database\Connectors\ConnectionFactory

namespace App\Helpers\MySQL;

use Config;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use InvalidArgumentException;
use PDO;

/**
 * Class YourAppConnectionFactory
 *
 * @package App\Helpers\MySQL
 */
class YourAppConnectionFactory extends ConnectionFactory
{
    /**
     * Create a new connection instance.
     *
     * @param  string   $driver
     * @param  PDO     $connection
     * @param  string   $database
     * @param  string   $prefix
     * @param  array    $config
     * @return \Illuminate\Database\Connection
     *
     * @throws InvalidArgumentException
     */
    protected function createConnection($driver, PDO $connection, $database, $prefix = '', array $config = [])
    {
        if ($this->container->bound($key = "db.connection.{$driver}")) {
            return $this->container->make($key, [$connection, $database, $prefix, $config]);
        }

        switch ($driver) {
            case 'mysql':
                if ($config['database'] === Config::get('database.connections.mysql.database')) {
                    return new DeadlockReadyMySqlConnection($connection, $database, $prefix, $config);
                } else {
                    return new MySqlConnection($connection, $database, $prefix, $config);
                }
            case 'pgsql':
                return new PostgresConnection($connection, $database, $prefix, $config);
            case 'sqlite':
                return new SQLiteConnection($connection, $database, $prefix, $config);
            case 'sqlsrv':
                return new SqlServerConnection($connection, $database, $prefix, $config);
        }

        throw new InvalidArgumentException("Unsupported driver [$driver]");
    }
}

现在我们应该在Providers/AppServiceProvider.php中替换标准框架的数据库连接工厂(或创建新的服务提供商)

public function register()
{
    $this->app->singleton('db.factory', function ($app) {
        return new YourAppConnectionFactory($app);
    });
}

就是这样!现在,应该重新启动所有在死锁上失败的查询。

答案 1 :(得分:0)

我们将这项技术应用于Laravel 5.6,它似乎运行良好。清理了一些东西,并添加了一个命令来测试死锁。以下是我在上面的答案中修改过的代码:

<?php

namespace App\Database;

use App\Database\AutoRetryMySqlConnection;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Connection;

/**
 * Class DatabaseConnectionFactory
 *
 * @package App\Database
 */
class DatabaseConnectionFactory extends ConnectionFactory
{
    /**
     * Create a new connection instance.
     *
     * @param  string   $driver
     * @param  \PDO|\Closure     $connection
     * @param  string   $database
     * @param  string   $prefix
     * @param  array    $config
     * @return \Illuminate\Database\Connection
     *
     * @throws \InvalidArgumentException
     */
    protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
    {
        if ($driver !== 'mysql') {
            return parent::createConnection($driver, $connection, $database, $prefix, $config);
        }

        if ($resolver = Connection::getResolver($driver)) {
            return $resolver($connection, $database, $prefix, $config);
        }

        return new AutoRetryMySqlConnection($connection, $database, $prefix, $config);
    }
}

AutoRetryMySqlConnection:

<?php

namespace App\Database;

use Closure;
use Exception;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\QueryException;
use Log;
use PDOException;

/**
 * Class AutoRetryMySqlConnection
 *
 * @package App\Helpers
 */
class AutoRetryMySqlConnection extends MySqlConnection
{
    /**
     * Error code of deadlock exception
     */
    const DEADLOCK_ERROR_CODE = 40001;

    /**
     * Number of attempts to retry
     */
    const ATTEMPTS_COUNT = 3;

    /**
     * Run a SQL statement.
     *
     * @param  string    $query
     * @param  array     $bindings
     * @param  \Closure  $callback
     * @return mixed
     *
     * @throws \Illuminate\Database\QueryException
     */
    protected function runQueryCallback($query, $bindings, Closure $callback)
    {
        $attempts_count = self::ATTEMPTS_COUNT;

        for ($attempt = 1; $attempt <= $attempts_count; $attempt++) {
            try {
                return parent::runQueryCallback($query, $bindings, $callback);
            } catch (QueryException $e) {
                if ($attempt > $attempts_count) {
                    throw $e;
                }

                if (!$this->shouldRetry($errorCode = $e->getCode())) {
                    throw $e;
                }

                $this->logRetry($attempt, $attempts_count, $bindings, $query, $errorCode);
            }
        }
    }

    /**
     * Use the provided error code to determine if the transaction should be retried.
     *
     * @param string|integer $errorCode
     * 
     * @return boolean
     */
    protected function shouldRetry($errorCode) {
        return (int) $errorCode === self::DEADLOCK_ERROR_CODE;
    }

    /**
     * Log when a transaction is automatically retried.
     *
     * @param integer $attempt
     * @param integer $attempts_count
     * @param array $bindings
     * @param string $query
     * @param string $errorCode
     * @return void
     */
    protected function logRetry($attempt, $attempts_count, $bindings, $query, $errorCode) {
        $sql = str_replace_array('\?', $this->prepareBindings($bindings), $query);

        Log::warning("Transaction has been restarted due to error {$errorCode}. Attempt {$attempt}/{$attempts_count}. SQL: {$sql}");
    }
}

DatabaseServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Database\DatabaseConnectionFactory;

/**
 * Class DatabaseServiceProvider
 *
 * @package App\Providers
 */
class DatabaseServiceProvider extends ServiceProvider
{
    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('db.factory', function ($app) {
            return new DatabaseConnectionFactory($app);
        });
    }
}

测试死锁的命令正在运行:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class ForceDeadlock extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'deadlock:force';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Force a database deadlock for testing purposes.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        if (App::environment('staging', 'hotfix', 'production')) {
            return $this->error('Command not available in this environment.');
        }

        $this->ask('Ready to create a dummy table123?');

        DB::statement('CREATE TABLE `table123` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `marks` INT NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;');

        DB::statement('INSERT INTO table123 (id, name, marks) VALUES (1, "abc", 5);');

        DB::statement('INSERT INTO table123 (id, name, marks) VALUES (2, "xyz", 1);');

        $this->info('Created table123 to test deadlock.');

        $this->ask('Would you like to begin?');

        DB::statement('begin;');
        DB::statement('UPDATE table123 SET marks=marks-1 WHERE id=1;');

        $this->info('Open a MySQL connection, switch to this database, and paste the following:');

        $this->info('BEGIN;');
        $this->info('UPDATE table123 SET marks=marks+1 WHERE id=2;');
        $this->info('UPDATE table123 SET marks=marks-1 WHERE id=1;');
        $this->info('COMMIT;');

        $this->ask('Are you ready to test the deadlock?');

        DB::statement('UPDATE table123 SET marks=marks+1 WHERE id=2;');
        DB::statement('COMMIT;');

        $this->info('Open the laravel.log file and confirm a deadlock was retried.');

        $this->ask('Ready to drop the test123 table?');

        DB::statement('DROP TABLE table123;');
    }
}

答案 2 :(得分:0)

这是在Laravel 5中进行的操作(已在5.7和5.8版上测试):

$numberOfAttempts = 5; // how many times the transaction will retry
// Since we are passing a closure, we need to send
// any "external" variables to it with the `use` keyword
DB::transaction(function () use ($user, $somethingElse) {
    // this is just an example
    $user->update(...);
    $somethingElse->delete();
}, $numberOfAttempts);