J'ai une table MySQL avec environ 5 000 000 de lignes qui sont constamment mises à jour de petites manières par des processus Perl parallèles se connectant via DBI. Le tableau comprend environ 10 colonnes et plusieurs index.
Une opération assez courante donne parfois lieu à l'erreur suivante:
DBD::mysql::st execute failed: Deadlock found when trying to get lock; try restarting transaction at Db.pm line 276.
L'instruction SQL qui déclenche l'erreur ressemble à ceci:
UPDATE file_table SET a_lock = 'process-1234' WHERE param1 = 'X' AND param2 = 'Y' AND param3 = 'Z' LIMIT 47
L'erreur n'est déclenchée que parfois. J'évaluerais en 1% des appels ou moins. Cependant, cela ne s'est jamais produit avec une petite table et est devenu plus courant à mesure que la base de données s'est développée.
Notez que j'utilise le champ a_lock dans file_table pour m'assurer que les quatre processus presque identiques que j'exécute n'essaient pas de travailler sur la même ligne. La limite est conçue pour diviser leur travail en petits morceaux.
Je n'ai pas fait beaucoup de réglages sur MySQL ou DBD :: mysql. MySQL est un déploiement Solaris standard et la connexion à la base de données est configurée comme suit:
my $dsn = "DBI:mysql:database=" . $DbConfig::database . ";Host=${DbConfig::hostname};port=${DbConfig::port}";
my $dbh = DBI->connect($dsn, $DbConfig::username, $DbConfig::password, { RaiseError => 1, AutoCommit => 1 }) or die $DBI::errstr;
J'ai vu en ligne que plusieurs autres personnes ont signalé des erreurs similaires et qu'il peut s'agir d'une véritable impasse.
J'ai deux questions:
Qu'en est-il exactement de ma situation à l'origine de l'erreur ci-dessus?
Existe-t-il un moyen simple de contourner ce problème ou de réduire sa fréquence? Par exemple, comment dois-je procéder exactement pour "redémarrer la transaction sur la ligne 276 de Db.pm"?
Merci d'avance.
Si vous utilisez InnoDB ou tout SGBDR transactionnel de niveau ligne, il est possible que any la transaction d'écriture puisse entraîner une impasse, même dans des situations parfaitement normales. Des tables plus grandes, des écritures plus importantes et de longs blocs de transaction augmentent souvent la probabilité de blocage. Dans votre situation, c'est probablement une combinaison de ceux-ci.
La seule façon de vraiment gérer les blocages est d'écrire votre code pour les attendre. Ce n'est généralement pas très difficile si votre code de base de données est bien écrit. Souvent, vous pouvez simplement mettre un try/catch
autour de la logique d'exécution de la requête et recherchez un blocage lorsque des erreurs se produisent. Si vous en attrapez un, la chose normale à faire est simplement de tenter d'exécuter à nouveau la requête ayant échoué.
Je vous recommande fortement de lire cette page dans le manuel MySQL. Il a une liste de choses à faire pour faire face aux blocages et réduire leur fréquence.
La réponse est correcte, cependant la documentation Perl sur la façon de gérer les blocages est un peu clairsemée et peut-être déroutante avec les options PrintError, RaiseError et HandleError. Il semble que plutôt que d'utiliser HandleError, utilisez Print et Raise, puis utilisez quelque chose comme Try: Tiny pour encapsuler votre code et vérifier les erreurs. Le code ci-dessous donne un exemple où le code db se trouve dans une boucle while qui réexécutera une instruction sql erronée toutes les 3 secondes. Le bloc catch obtient $ _ qui est le message d'erreur spécifique. Je passe cela à une fonction de gestionnaire "dbi_err_handler" qui vérifie $ _ contre un hôte d'erreurs et renvoie 1 si le code doit continuer (rompant ainsi la boucle) ou 0 si c'est un blocage et doit être réessayé ...
$sth = $dbh->prepare($strsql);
my $db_res=0;
while($db_res==0)
{
$db_res=1;
try{$sth->execute($param1,$param2);}
catch
{
print "caught $_ in insertion to hd_item_upc for upc $upc\n";
$db_res=dbi_err_handler($_);
if($db_res==0){sleep 3;}
}
}
dbi_err_handler doit avoir au moins les éléments suivants:
sub dbi_err_handler
{
my($message) = @_;
if($message=~ m/DBD::mysql::st execute failed: Deadlock found when trying to get lock; try restarting transaction/)
{
$caught=1;
$retval=0; # we'll check this value and sleep/re-execute if necessary
}
return $retval;
}
Vous devez inclure les autres erreurs que vous souhaitez gérer et définir $ retval selon que vous souhaitez réexécuter ou continuer.
J'espère que cela aide quelqu'un -
Notez que si vous utilisez SELECT FOR UPDATE
pour effectuer une vérification d'unicité avant l'insertion, vous obtiendrez un blocage pour chaque condition de concurrence, sauf si vous activez innodb_locks_unsafe_for_binlog
option. Une méthode sans blocage pour vérifier l'unicité consiste à insérer aveuglément une ligne dans une table avec un index unique à l'aide de INSERT IGNORE
, puis pour vérifier le nombre de lignes concernées.
ajouter la ligne ci-dessous à my.cnf
fichier
innodb_locks_unsafe_for_binlog = 1
1 - ON
0 - NON
L'idée de réessayer la requête en cas d'exception Deadlock est bonne, mais elle peut être terriblement lente, car la requête mysql continuera d'attendre la libération des verrous. Et en cas de blocage, mysql essaie de trouver s'il y a un blocage, et même après avoir découvert qu'il y a un blocage, il attend un moment avant de lancer un thread afin de sortir de la situation de blocage.
Ce que j'ai fait lorsque j'ai fait face à cette situation, c'est d'implémenter le verrouillage dans votre propre code, car c'est le mécanisme de verrouillage de mysql qui échoue en raison d'un bogue. J'ai donc implémenté mon propre verrouillage de niveau ligne dans mon Java:
private HashMap<String, Object> rowIdToRowLockMap = new HashMap<String, Object>();
private final Object hashmapLock = new Object();
public void handleShortCode(Integer rowId)
{
Object lock = null;
synchronized(hashmapLock)
{
lock = rowIdToRowLockMap.get(rowId);
if (lock == null)
{
rowIdToRowLockMap.put(rowId, lock = new Object());
}
}
synchronized (lock)
{
// Execute your queries on row by row id
}
}