当我使用Laravel
Laravel Eloquent
应用程序出现此错误
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found
如何在完成查询之前重新执行查询?
答案 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);