À proprement parler, votre test d'unicité ne garantira pas l'unicité sous une charge simultanée. Le problème est que vous vérifiez l'unicité avant (et séparément de) l'endroit où vous insérez une ligne pour "réclamer" votre code d'accès nouvellement généré. Un autre processus pourrait faire la même chose, en même temps. Voici comment cela se passe...
Deux processus génèrent exactement le même mot de passe. Ils commencent chacun par vérifier l'unicité. Étant donné qu'aucun des processus n'a (encore) inséré de ligne dans la table, les deux processus ne trouveront aucun mot de passe correspondant dans la base de données, et les deux processus supposeront donc que le code est unique. Maintenant que les processus continuent chacun leur travail, ils finiront par tous les deux insérer une ligne dans les files
table en utilisant le code généré -- et ainsi vous obtenez un doublon.
Pour contourner ce problème, vous devez effectuer la vérification et effectuer l'insertion en une seule opération "atomique". Voici une explication de cette approche :
Si vous voulez que le mot de passe soit unique, vous devez définir la colonne de votre base de données comme UNIQUE
. Cela garantira l'unicité (même si votre code php ne le fait pas) en refusant d'insérer une ligne qui entraînerait un mot de passe en double.
CREATE TABLE files (
id int(10) unsigned NOT NULL auto_increment PRIMARY KEY,
filename varchar(255) NOT NULL,
passcode varchar(64) NOT NULL UNIQUE,
)
Maintenant, utilisez le SHA1()
de mysql et NOW()
pour générer votre mot de passe dans le cadre de partie de l'instruction d'insertion. Combinez ceci avec INSERT IGNORE ...
(docs
), et bouclez jusqu'à ce qu'une ligne soit insérée avec succès :
do {
$query = "INSERT IGNORE INTO files
(filename, passcode) values ('whatever', SHA1(NOW()))";
$res = mysql_query($query);
} while( $res && (0 == mysql_affected_rows()) )
if( !$res ) {
// an error occurred (eg. lost connection, insufficient permissions on table, etc)
// no passcode was generated. handle the error, and either abort or retry.
} else {
// success, unique code was generated and inserted into db.
// you can now do a select to retrieve the generated code (described below)
// or you can proceed with the rest of your program logic.
}
Remarque : L'exemple ci-dessus a été modifié pour tenir compte des excellentes observations publiées par @martinstoeckli dans la section des commentaires. Les modifications suivantes ont été apportées :
- modifié
mysql_num_rows()
(docs ) àmysql_affected_rows()
(docs ) -- num_rows ne s'applique pas aux insertions. Également supprimé l'argument demysql_affected_rows()
, car cette fonction opère au niveau de la connexion, pas au niveau du résultat (et dans tous les cas, le résultat d'une insertion est booléen, pas un numéro de ressource). - ajout d'une vérification d'erreur dans la condition de boucle et ajout d'un test d'erreur/succès après la sortie de la boucle. La gestion des erreurs est importante, car sans elle, les erreurs de base de données (comme les connexions perdues ou les problèmes d'autorisations) feront tourner la boucle pour toujours. L'approche ci-dessus (en utilisant
IGNORE
, etmysql_affected_rows()
, et tester$res
séparément pour les erreurs) nous permet de distinguer ces "erreurs de base de données réelles" de la violation de contrainte unique (qui est une condition de non-erreur complètement valide dans cette section de logique).
Si vous avez besoin d'obtenir le mot de passe après qu'il ait été généré, sélectionnez à nouveau l'enregistrement :
$res = mysql_query("SELECT * FROM files WHERE id=LAST_INSERT_ID()");
$row = mysql_fetch_assoc($res);
$passcode = $row['passcode'];
Modifier :modification de l'exemple ci-dessus pour utiliser la fonction mysql LAST_INSERT_ID()
, plutôt que la fonction de PHP. C'est un moyen plus efficace d'accomplir la même chose, et le code résultant est plus propre, plus clair et moins encombré.